A plugin of In App Purchase for Tauri on MacOS.
-
Add dependencies in file
src-tauri/Cargo.toml
:[target.'cfg(target_os = "macos")'.dependencies] tauri-plugin-iap = { git = "https://github.com/wtto00/tauri-plugin-iap", tag = "v0.0.1" }
-
Add dependencies of front-end:
pnpm add https://github.com/wtto00/tauri-plugin-iap.git # yarn add https://github.com/wtto00/tauri-plugin-iap.git # npm i --save https://github.com/wtto00/tauri-plugin-iap.git
-
Enable plugins in
src-tauri/main.rs
fn main() { tauri::Builder::default() .setup(move |app| { // Add this line #[cfg(target_os="macos")] app.app_handle().plugin(tauri_plugin_iap::init())?; Ok(()) }) .run(tauri::generate_context!()) .expect("error while running tauri application"); }
-
This plugin requires a minimum MacOS version of 10.15 or higher, so you must configure it in
src-tauri/tauri.config.json
.{ "tauri": { "bundle": { "macOS": { "minimumSystemVersion": "10.15" } } } }
-
You can only test IAP in a signed app. Since
Tauri
cannot be signed in development environment, you'll need to sign it and upload it toAppConnect
for testing, then install and test it fromTestFlight
. See details at: tauri-apps/tauri#7930About uploading to
AppConnect
, see Publish
import {
canMakePayments,
initialize,
startQueryProducts,
TransactionStatus,
finishTransaction,
requestPruchase,
restorePurchases,
type Product,
type Transaction,
type Exception,
} from "tauri-plugin-iap-api";
// This maybe fetch from your own server api
const product_identifiers = ["product_id_1", "product_id_2"];
// The verified and available products are displayed in the interface.
let products_validated = [];
// TODO: Send receiptData to your own server api to validate the transaction is valid or not.
// You should cache verified receiptData or transactionId to avoid repeatedly verifying the same data. About caching transactionId, you can refer to https://stackoverflow.com/questions/45705069/ios-storekit-transaction-identifier-does-not-match-receipt
function validate(transaction: Transaction) {
return true;
}
function onProductsUpdated(products: Product[]) {
products_validated.push(...products);
}
async function onTransactionsUpdated(transactions: Transaction[]) {
for await (const transaction of transactions) {
if (transaction.status === TransactionStatus.pending) {
// Just Show loading
continue;
}
let msg = "";
if (transaction.status === TransactionStatus.failed) {
msg ||= transaction.error || "Something wrong.";
} else if (
transaction.status === TransactionStatus.purchased ||
transaction.status === TransactionStatus.restored
) {
const isValid = await validate();
if (isValid) {
// TODO: Distribute the verified purchased items to the user.
msg ||= "Success";
// if this is not called a transaction will keep being triggered automatically on app start
finishTransaction(transaction.transactionId);
}
}
if (msg) {
// Just toast msg
}
}
}
function onRestoreCompleted() {
// If user only purchases items that are not restorable, such as a non-renewing subscription or a consumable product.
// In this situation, `onTransactionsUpdated` cannot be called back.
// So you can inform the user that the restoration has been completed through this method.
}
function onException(err: Exception) {
// Just toast some error message
}
if (await canMakePayments()) {
const inited = await initialize({
onProductsUpdated,
onTransactionsUpdated,
onRestoreCompleted,
onException,
});
if (inited) {
startQueryProducts(product_identifiers);
}
}
// restore finished purchase
restorePurchases();
// request a purchase
requestPruchase(product_identifiers[0]);
if the payment platform is ready and available or not.
const isAvailable = await canMakePayments();
The three-letter code that represents the country or region associated with the App Store storefront.
const code = await countryCode();
Initialize the plugin.
If the initialization is not successful, you cannot call the startQueryProducts
, restorePurchases
, requestPurchase
, finishTransaction
interfaces.
const inited = await initialize({
onProductsUpdated: (products: Product[]) => {},
onTransactionsUpdated: async (transactions: Transaction[]) => {},
onRestoreCompleted: () => {},
onException: (err: Exception) => {},
});
Requests product data from the App Store.
const productIdentifiers = ["com.example.productA", "com.example.productB"];
void startQueryProducts(productIdentifiers);
The query result is returned in the onProductsUpdated
callback in the initialize
function. Apple Document Link
Asks the payment queue to restore previously completed purchases.
void restorePurchases();
Request a purchase.
void requestPruchase("com.example.productA");
void finishTransaction("someTransactionId");
Please replace the COMPANY_NAME
, TEAM_ID
, APP_NAME
, APP_IDENTIFIER
with your specific information.
-
Preparation
-
Ensure that the certificate
3rd Party Mac Developer Application: COMPANY_NAME (TEAM_ID)
and3rd Party Mac Developer Installer: COMPANY_NAME (TEAM_ID)
is installed on the local machine.
You can see it inKeychain Access
app.
Created in https://developer.apple.com/account/resources/certificates/add, and selectMac App Distribution
,Mac Installer Distribution
inSoftware
section. -
Make sure to download the correct provision profile file from AppConnect to
src-tauri/entitlements/Mac_App_Distribution.provisionprofile
.
Ceated in https://developer.apple.com/account/resources/profiles/add, and selectMac App Store Connect
inDistribution
section. -
Ensure that the entitlements file has been created in
src-tauri/entitlements/APP_NAME.entitlements
.
Reference content is as follows:<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>com.apple.security.app-sandbox</key> <true/> <key>com.apple.security.network.client</key> <true/> <key>com.apple.security.files.user-selected.read-write</key> <true/> <key>com.apple.application-identifier</key> <string>TEAM_ID.APP_IDENTIFIER</string> <key>com.apple.developer.team-identifier</key> <string>TEAM_ID</string> </dict> </plist>
The list of entitlements can be found here. If you want to publish an app on the App Store, you need to ensure that it does not include unused entitlements.
-
-
Execute the following script
unset APPLE_SIGNING_IDENTITY unset APPLE_CERTIFICATE sign_app="3rd Party Mac Developer Application: COMPANY_NAME (TEAM_ID)" sign_install="3rd Party Mac Developer Installer: COMPANY_NAME (TEAM_ID)" profile="src-tauri/entitlements/Mac_App_Distribution.provisionprofile" target="universal-apple-darwin" npx tauri build --target "${target}" --verbose # cargo tauri build --target "${target}" --verbose app_path="src-tauri/target/${target}/release/bundle/macos/APP_NAME.app" build_name="src-tauri/target/${target}/release/bundle/macos/APP_NAME.pkg" cp_dir="src-tauri/target/${target}/release/bundle/macos/APP_NAME.app/Contents/embedded.provisionprofile" entitlements="src-tauri/entitlements/APP_NAME.entitlements" cp "${profile}" "${cp_dir}" codesign --deep --force -s "${sign_app}" --entitlements ${entitlements} "${app_path}" productbuild --component "${app_path}" /Applications/ --sign "${sign_install}" "${build_name}"
-
Upload to AppConnect
Now you will find the filesrc-tauri/target/${target}/release/bundle/macos/APP_NAME.pkg
. Upload this pkg file toAppConnect
byTransporter
. -
Install from TestFlight
After you upload to
AppConnect
, you can see the app you just uploaded onTestFlight
a few minutes later, install it, and test it.
You can see the debug message of this plugin by this command:
log stream --level debug --predicate 'subsystem == "tauri" && category == "plugin.apple.iap"'