@choochmeque/tauri-plugin-iap-api 0.4.5 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,12 @@
1
1
  [![NPM Version](https://img.shields.io/npm/v/@choochmeque%2Ftauri-plugin-iap-api)](https://www.npmjs.com/package/@choochmeque/tauri-plugin-iap-api)
2
2
  [![Crates.io Version](https://img.shields.io/crates/v/tauri-plugin-iap)](https://crates.io/crates/tauri-plugin-iap)
3
+ [![Tests](https://github.com/Choochmeque/tauri-plugin-iap/actions/workflows/tests.yml/badge.svg)](https://github.com/Choochmeque/tauri-plugin-iap/actions/workflows/tests.yml)
4
+ [![codecov](https://codecov.io/gh/Choochmeque/tauri-plugin-iap/branch/main/graph/badge.svg)](https://codecov.io/gh/Choochmeque/tauri-plugin-iap)
3
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
4
6
 
5
7
  # Tauri Plugin IAP
6
8
 
7
- A Tauri plugin for In-App Purchases (IAP) with support for subscriptions on iOS (StoreKit 2), Android (Google Play Billing) and Windows (Microsoft Store).
9
+ A Tauri plugin for In-App Purchases (IAP) with support for subscriptions on iOS (StoreKit 2), Android (Google Play Billing), Windows (Microsoft Store) and macOS (StoreKit 2).
8
10
 
9
11
  ## Features
10
12
 
@@ -26,7 +28,7 @@ A Tauri plugin for In-App Purchases (IAP) with support for subscriptions on iOS
26
28
  - **iOS**: StoreKit 2 (requires iOS 15.0+)
27
29
  - **Android**: Google Play Billing Library v8.0.0
28
30
  - **Windows**: Microsoft Store API (Windows 10/11)
29
- - **macOS**: Experimental support - might not work correctly (still required some work to do)
31
+ - **macOS**: StoreKit 2 (requires macOS 13.0+)
30
32
 
31
33
  ## Installation
32
34
 
@@ -44,7 +46,7 @@ Add the plugin to your Tauri project's `Cargo.toml`:
44
46
 
45
47
  ```toml
46
48
  [dependencies]
47
- tauri-plugin-iap = "0.4"
49
+ tauri-plugin-iap = "0.5"
48
50
  ```
49
51
 
50
52
  Configure the plugin permissions in your `capabilities/default.json`:
@@ -68,6 +70,39 @@ fn main() {
68
70
  }
69
71
  ```
70
72
 
73
+ ## Example App
74
+
75
+ An example application is available in the [`examples/iap-demo`](examples/iap-demo) directory. The example demonstrates all core IAP functionality with a UI:
76
+
77
+ - Initialize IAP connection
78
+ - Fetch and display products with pricing
79
+ - Purchase products and subscriptions
80
+ - Restore previous purchases
81
+ - Check product ownership status
82
+ - Real-time purchase update notifications
83
+
84
+ ### Running the Example
85
+
86
+ ```bash
87
+ cd examples/iap-demo
88
+
89
+ # Install dependencies
90
+ pnpm install
91
+
92
+ # Run in development mode
93
+ pnpm tauri dev
94
+
95
+ # Build for production
96
+ pnpm tauri build
97
+ ```
98
+
99
+ **Important:** The app must be properly code-signed to test IAP functionality:
100
+ - **iOS/macOS**: Code signing is required by StoreKit. Configure signing in Xcode or via Tauri's configuration.
101
+ - **Android**: Sign your APK/AAB with a release keystore for production testing.
102
+ - **Windows**: App must be associated with Microsoft Store and signed for Store submission.
103
+
104
+ The example app provides a fully functional demo with inline documentation for each IAP feature, making it easy to understand how to integrate the plugin into your own application.
105
+
71
106
  ## Usage
72
107
 
73
108
  ### JavaScript/TypeScript
@@ -82,7 +117,7 @@ import {
82
117
  getProductStatus,
83
118
  onPurchaseUpdated,
84
119
  PurchaseState
85
- } from 'tauri-plugin-iap-api';
120
+ } from '@choochmeque/tauri-plugin-iap-api';
86
121
 
87
122
  // Initialize the billing client
88
123
  await initialize();
@@ -126,12 +161,12 @@ const restored = await restorePurchases('subs');
126
161
  await acknowledgePurchase(purchaseResult.purchaseToken);
127
162
 
128
163
  // Listen for purchase updates
129
- const unlisten = onPurchaseUpdated((purchase) => {
164
+ const listener = await onPurchaseUpdated((purchase) => {
130
165
  console.log('Purchase updated:', purchase);
131
166
  });
132
167
 
133
168
  // Stop listening
134
- unlisten();
169
+ await listener.unregister();
135
170
  ```
136
171
 
137
172
  ## Platform Setup
@@ -161,6 +196,37 @@ unlisten();
161
196
  3. Associate your app with the Microsoft Store
162
197
  4. Test with Windows sandbox environment
163
198
 
199
+ ### macOS Setup
200
+
201
+ 1. Configure your app in App Store Connect
202
+ 2. Create subscription or in-app purchase products with pricing
203
+ 3. Configure code signing in `tauri.conf.json`:
204
+ ```json
205
+ {
206
+ "bundle": {
207
+ "macOS": {
208
+ "signingIdentity": "Developer ID Application: Your Name (TEAM_ID)",
209
+ "entitlements": "path/to/entitlements.plist"
210
+ }
211
+ }
212
+ }
213
+ ```
214
+ 4. Add required entitlements for StoreKit (create `entitlements.plist`):
215
+ ```xml
216
+ <?xml version="1.0" encoding="UTF-8"?>
217
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
218
+ <plist version="1.0">
219
+ <dict>
220
+ <key>com.apple.security.app-sandbox</key>
221
+ <true/>
222
+ <key>com.apple.security.network.client</key>
223
+ <true/>
224
+ </dict>
225
+ </plist>
226
+ ```
227
+ 5. Test with sandbox accounts or StoreKit Configuration files
228
+ 6. **Important**: App must be code-signed to use StoreKit APIs
229
+
164
230
  ## API Reference
165
231
 
166
232
  ### `initialize()`
@@ -221,9 +287,11 @@ Checks the ownership and subscription status of a specific product.
221
287
  - `isAcknowledged`: Whether the purchase has been acknowledged
222
288
  - `purchaseToken`: Token for the purchase transaction
223
289
 
224
- ### `onPurchaseUpdated(callback: (purchase: Purchase) => void)`
290
+ ### `onPurchaseUpdated(callback: (purchase: Purchase) => void): Promise<PluginListener>`
225
291
  Listens for purchase state changes.
226
292
 
293
+ **Returns:** A `PluginListener` object with an `unregister()` method to stop listening.
294
+
227
295
  ## Differences Between Platforms
228
296
 
229
297
  ### iOS (StoreKit 2)
@@ -244,6 +312,13 @@ Listens for purchase state changes.
244
312
  - Supports consumables, durables, and subscriptions
245
313
  - Uses SKUs for subscription offer variations
246
314
 
315
+ ### macOS (StoreKit 2)
316
+ - Same StoreKit 2 API as iOS
317
+ - Automatic transaction verification
318
+ - No manual acknowledgment needed
319
+ - Requires macOS 13.0+
320
+ - App must be code-signed (StoreKit requires valid signature)
321
+
247
322
  ## Testing
248
323
 
249
324
  ### iOS
@@ -262,6 +337,12 @@ Listens for purchase state changes.
262
337
  3. Test with Windows Dev Center test payment methods
263
338
  4. Ensure app is associated with Store listing
264
339
 
340
+ ### macOS
341
+ 1. Use sandbox test accounts (same as iOS)
342
+ 2. Use StoreKit Configuration files for local testing
343
+ 3. App must be code-signed to use StoreKit
344
+ 4. Clear purchase history in System Settings > App Store > Sandbox Account
345
+
265
346
  ## License
266
347
 
267
348
  [MIT](LICENSE)
package/dist-js/index.cjs CHANGED
@@ -1,7 +1,6 @@
1
1
  'use strict';
2
2
 
3
3
  var core = require('@tauri-apps/api/core');
4
- var event = require('@tauri-apps/api/event');
5
4
 
6
5
  /**
7
6
  * Purchase state enumeration
@@ -173,10 +172,10 @@ async function getProductStatus(productId, productType = "subs") {
173
172
  * This event is triggered when a purchase state changes.
174
173
  *
175
174
  * @param callback - Function to call when a purchase is updated
176
- * @returns Cleanup function to stop listening
175
+ * @returns Promise resolving to a PluginListener that can be used to stop listening
177
176
  * @example
178
177
  * ```typescript
179
- * const unsubscribe = onPurchaseUpdated((purchase) => {
178
+ * const listener = await onPurchaseUpdated((purchase) => {
180
179
  * console.log(`Purchase updated: ${purchase.productId}`);
181
180
  * if (purchase.purchaseState === PurchaseState.PURCHASED) {
182
181
  * // Handle successful purchase
@@ -184,16 +183,11 @@ async function getProductStatus(productId, productType = "subs") {
184
183
  * });
185
184
  *
186
185
  * // Later, stop listening
187
- * unsubscribe();
186
+ * await listener.unregister();
188
187
  * ```
189
188
  */
190
- function onPurchaseUpdated(callback) {
191
- const unlisten = event.listen("purchaseUpdated", (event) => {
192
- callback(event.payload);
193
- });
194
- return () => {
195
- unlisten.then((fn) => fn());
196
- };
189
+ async function onPurchaseUpdated(callback) {
190
+ return await core.addPluginListener("iap", "purchaseUpdated", callback);
197
191
  }
198
192
 
199
193
  exports.acknowledgePurchase = acknowledgePurchase;
@@ -1,3 +1,4 @@
1
+ import { PluginListener } from "@tauri-apps/api/core";
1
2
  /**
2
3
  * Response from IAP initialization
3
4
  */
@@ -28,13 +29,21 @@ export interface SubscriptionOffer {
28
29
  * Product information from the app store
29
30
  */
30
31
  export interface Product {
32
+ /** Unique product identifier as configured in the app store */
31
33
  productId: string;
34
+ /** Localized product title */
32
35
  title: string;
36
+ /** Localized product description */
33
37
  description: string;
38
+ /** Type of product: "subs" for subscriptions, "inapp" for one-time purchases */
34
39
  productType: string;
40
+ /** Localized price string with currency symbol (e.g., "$9.99") */
35
41
  formattedPrice?: string;
42
+ /** ISO 4217 currency code (e.g., "USD", "EUR") */
36
43
  priceCurrencyCode?: string;
44
+ /** Price in micros (price × 1,000,000). For example, $9.99 = 9990000 */
37
45
  priceAmountMicros?: number;
46
+ /** Subscription offer details including pricing phases. (Android only) */
38
47
  subscriptionOfferDetails?: SubscriptionOffer[];
39
48
  }
40
49
  /**
@@ -47,16 +56,28 @@ export interface GetProductsResponse {
47
56
  * Purchase transaction information
48
57
  */
49
58
  export interface Purchase {
59
+ /** Unique order identifier from the store. May be undefined for pending purchases. */
50
60
  orderId?: string;
61
+ /** Application package name (Android) or bundle identifier (iOS/macOS) */
51
62
  packageName: string;
63
+ /** Product identifier that was purchased */
52
64
  productId: string;
65
+ /** Unix timestamp (milliseconds) when the purchase was made */
53
66
  purchaseTime: number;
67
+ /** Token used to identify this purchase for acknowledgment and server-side verification */
54
68
  purchaseToken: string;
55
- purchaseState: number;
69
+ /** Current state of the purchase. */
70
+ purchaseState: PurchaseState;
71
+ /** Whether this subscription is set to auto-renew. Always false for one-time purchases. */
56
72
  isAutoRenewing: boolean;
73
+ /** Whether the purchase has been acknowledged. Unacknowledged purchases are refunded after 3 days. (Android only, always true on iOS/macOS) */
57
74
  isAcknowledged: boolean;
75
+ /** Raw JSON response from the store for server-side verification. (Android only) */
58
76
  originalJson: string;
77
+ /** Cryptographic signature for purchase verification. (Android only) */
59
78
  signature: string;
79
+ /** Original transaction ID. Used to link renewals and restores to the original purchase. (iOS/macOS only) */
80
+ originalId?: string;
60
81
  }
61
82
  /**
62
83
  * Response containing restored purchases
@@ -244,10 +265,10 @@ export declare function getProductStatus(productId: string, productType?: "subs"
244
265
  * This event is triggered when a purchase state changes.
245
266
  *
246
267
  * @param callback - Function to call when a purchase is updated
247
- * @returns Cleanup function to stop listening
268
+ * @returns Promise resolving to a PluginListener that can be used to stop listening
248
269
  * @example
249
270
  * ```typescript
250
- * const unsubscribe = onPurchaseUpdated((purchase) => {
271
+ * const listener = await onPurchaseUpdated((purchase) => {
251
272
  * console.log(`Purchase updated: ${purchase.productId}`);
252
273
  * if (purchase.purchaseState === PurchaseState.PURCHASED) {
253
274
  * // Handle successful purchase
@@ -255,7 +276,7 @@ export declare function getProductStatus(productId: string, productType?: "subs"
255
276
  * });
256
277
  *
257
278
  * // Later, stop listening
258
- * unsubscribe();
279
+ * await listener.unregister();
259
280
  * ```
260
281
  */
261
- export declare function onPurchaseUpdated(callback: (purchase: Purchase) => void): () => void;
282
+ export declare function onPurchaseUpdated(callback: (purchase: Purchase) => void): Promise<PluginListener>;
package/dist-js/index.js CHANGED
@@ -1,5 +1,4 @@
1
- import { invoke } from '@tauri-apps/api/core';
2
- import { listen } from '@tauri-apps/api/event';
1
+ import { invoke, addPluginListener } from '@tauri-apps/api/core';
3
2
 
4
3
  /**
5
4
  * Purchase state enumeration
@@ -171,10 +170,10 @@ async function getProductStatus(productId, productType = "subs") {
171
170
  * This event is triggered when a purchase state changes.
172
171
  *
173
172
  * @param callback - Function to call when a purchase is updated
174
- * @returns Cleanup function to stop listening
173
+ * @returns Promise resolving to a PluginListener that can be used to stop listening
175
174
  * @example
176
175
  * ```typescript
177
- * const unsubscribe = onPurchaseUpdated((purchase) => {
176
+ * const listener = await onPurchaseUpdated((purchase) => {
178
177
  * console.log(`Purchase updated: ${purchase.productId}`);
179
178
  * if (purchase.purchaseState === PurchaseState.PURCHASED) {
180
179
  * // Handle successful purchase
@@ -182,16 +181,11 @@ async function getProductStatus(productId, productType = "subs") {
182
181
  * });
183
182
  *
184
183
  * // Later, stop listening
185
- * unsubscribe();
184
+ * await listener.unregister();
186
185
  * ```
187
186
  */
188
- function onPurchaseUpdated(callback) {
189
- const unlisten = listen("purchaseUpdated", (event) => {
190
- callback(event.payload);
191
- });
192
- return () => {
193
- unlisten.then((fn) => fn());
194
- };
187
+ async function onPurchaseUpdated(callback) {
188
+ return await addPluginListener("iap", "purchaseUpdated", callback);
195
189
  }
196
190
 
197
191
  export { PurchaseState, acknowledgePurchase, getProductStatus, getProducts, getPurchaseHistory, initialize, onPurchaseUpdated, purchase, restorePurchases };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@choochmeque/tauri-plugin-iap-api",
3
- "version": "0.4.5",
3
+ "version": "0.5.1",
4
4
  "license": "MIT",
5
5
  "author": "You",
6
6
  "description": "A Tauri v2 plugin that enables In-App Purchases (IAP)",
@@ -37,15 +37,23 @@
37
37
  },
38
38
  "devDependencies": {
39
39
  "@rollup/plugin-typescript": "^12.1.4",
40
+ "@vitest/coverage-v8": "^4.0.13",
41
+ "@vitest/ui": "^4.0.13",
42
+ "happy-dom": "^20.0.10",
43
+ "prettier": "3.7.4",
40
44
  "rollup": "^4.9.6",
41
45
  "tslib": "^2.6.2",
42
46
  "typescript": "^5.3.3",
43
- "prettier": "3.6.2"
47
+ "vitest": "^4.0.13"
44
48
  },
45
49
  "scripts": {
46
50
  "build": "rollup -c",
47
51
  "pretest": "pnpm build",
52
+ "test": "vitest run",
53
+ "test:watch": "vitest",
54
+ "test:ui": "vitest --ui",
55
+ "test:coverage": "vitest run --coverage",
48
56
  "format": "prettier --write \"./**/*.{cjs,mjs,js,jsx,mts,ts,tsx,html,css,json}\"",
49
- "format-check": "prettier --check \"./**/*.{cjs,mjs,js,jsx,mts,ts,tsx,html,css,json}\""
57
+ "format:check": "prettier --check \"./**/*.{cjs,mjs,js,jsx,mts,ts,tsx,html,css,json}\""
50
58
  }
51
59
  }