Written by Robert Koch
These are the high level steps that we'll follow to get In-App Purchasing working.
PRODUCT ID
here as we'll need it later.After you have created the purchase item you can setup a release. IAPs can only be published to users via a new release, similar to how you would update your app.
While they look different subscriptions and in-app purchases are the same thing behind the scenes. The only difference is that subscriptions are recurring payments.
In the new release you can select from the list of valid in-app purchases and subscriptions.
Once the purchase option has been added to the release it will appear in the submission, you can now create test builds of your app and once you're ready you can submit the app for review.
main
and renderer
. The main
process runs in a node environment and can run privilaged code like writing to
the filesystem. The renderer runs inside of a browser environment and is
restricted in what it can access. Because of these restrictions it's not
possible to directly call the In-App Purchase APIs from a browser window so
we'll need to use electrons InterProcess Communication (IPC) library.getProducts
and
purchaseProduct
function.1import { inAppPurchase } from 'electron/main'23app.on('ready', async () => {4 ipcMain.handle('getProducts', async () => {5 const products = await inAppPurchase.getProducts()6 console.log(products)7 return products8 })910 // This is a good way to test if the app is running in the Mac App Store11 ipcMain.handle('isMas', () => !!process.mas || process.env['ELECTRON_IS_MAS'])1213 ipcMain.handle(14 'purchaseProduct',15 (event, productIdentifier: string, quantity: number) => {16 console.log(`Purchasing ${productIdentifier}...`)17 console.log(`Quantity: ${quantity}`)18 console.log(`Event: ${event}`)19 return inAppPurchase.purchaseProduct(productIdentifier, quantity)20 }21 )22})
These functions are now registered to run when a singal from the renderer process is received in the main process.
We'll also need to register the functions in the frontend. The preload file runs before the web page loads in the renderer, this is when we can perform prvilaged operations like defining functions from the context bridge.
1declare global {2 namespace NodeJS {3 interface Global {4 ipcRenderer: IpcRenderer5 getProducts: () => Electron.Product[]6 purchaseProduct: (7 productIdentifier: string,8 quantity: number9 ) => Promise<boolean>10 isMas: () => boolean11 }12 }13}1415contextBridge.exposeInMainWorld('electronAPI', {16 getProducts: () => ipcRenderer.invoke('getProducts'),17 isMas: () => ipcRenderer.invoke('isMas'),18 purchaseProduct: (productIdentifier: string, quantity: number) =>19 ipcRenderer.invoke('purchaseProduct', productIdentifier, quantity),20})
Now with the IPC functions setup we can call them like normal functions from the frontend.
getProducts
looks like. It has all
the information you'd need to handle a purchase made in an app. You can now
create custom code to handle purchasing and subscriptions.suspense
feature was not stable.useMas
which contains the
context for whether the app is running in a Mac App Store environment. There are
a few advantages to this design - hooks can be used everywhere, immediately to
render a page so there is no need to use suspense
. The context of the
environment is also maintained throughout the app, no single view holds the
state of the MAS environment.isMas
function
returns. It will only do this when the context provider is intially mounted
which will happen when the app starts.1import { createContext, useContext, useLayoutEffect, useState } from "react";23type MasContextProps = boolean;45const MasContext = createContext<MasContextProps>(true);67export const MasProvider = ({ children }) => {8 const [_isMas, setMas] = useState<boolean>(true);910 useLayoutEffect(() => {11 // @ts-expect-error12 window.electronAPI.isMas().then(setMas);13 }, []);1415 return <MasContext.Provider value={_isMas}>{children}</MasContext.Provider>;16};1718export function useMas() {19 return useContext(MasContext);20}
main.ts
you can
manually set ELECTRON_IS_MAS
as an environment variable while developing to
test what the UI should look like in MAS mode, if you need to show a different
payment screen (you probably do) this is a great way to check it.This is all well and good but we can't actually test with the App Store until we create a build of the app - signed by Apple. You can create dev builds that can access your App Store sandbox account allowing you to make fake purchases without spending money.
macOS App Development
profile and a
Mac Development
certificate in the Apple Developer portal before you can build
the app. If you're using electron-builder
it should be as simple as running
the following command to create a new build.electron-builder --config electron-builder.config.ts --mac mas-dev
electron-builder
or have a look at some of the
many
other
blogs about Electron provisioning, or better yet check out my project on GitHub
to see how I've setup the build environment.Once complete your app will have a binary that can be run locally, if you have properly created a draft release in App Store Connect then this build will have access to the products linked to the release and you'll see a success popup when purchasing.
I hope this helps, I was stuck trying to figure out why I couldn't get In-App Purchases working for a while. In my instance it turns out I hadn't created a release which had the purchases linked. Please send me a tweet or a toot showing your Electron project!