@capgo/native-purchases 7.1.29 → 7.1.31

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
@@ -24,7 +24,7 @@ Complete visual testing guides for both platforms:
24
24
  | Platform | Guide | Content |
25
25
  |----------|-------|---------|
26
26
  | 🍎 **iOS** | **[iOS Testing Guide](./docs/iOS_TESTING_GUIDE.md)** | StoreKit Local Testing, Sandbox Testing, Developer Mode setup |
27
- | 🤖 **Android** | **[Android Testing Guide](./docs/Android_TESTING_GUIDE.md)** | Internal Testing, License Testing, Internal App Sharing |
27
+ | 🤖 **Android** | **[Android Testing Guide](./docs/ANDROID_TESTING_GUIDE.md)** | Internal Testing, License Testing, Internal App Sharing |
28
28
 
29
29
  > 💡 **Quick Start**: Choose **StoreKit Local Testing** for iOS or **Internal Testing** for Android for the fastest development experience.
30
30
 
@@ -81,18 +81,40 @@ For testing in-app purchases on iOS:
81
81
  Import the plugin in your TypeScript file:
82
82
 
83
83
  ```typescript
84
- import { NativePurchases } from '@capgo/native-purchases';
84
+ import { NativePurchases, PURCHASE_TYPE } from '@capgo/native-purchases';
85
85
  ```
86
86
 
87
+ ### ⚠️ Important: In-App vs Subscription Purchases
88
+
89
+ There are two types of purchases with different requirements:
90
+
91
+ | Purchase Type | productType | planIdentifier | Use Case |
92
+ |---------------|-------------|----------------|----------|
93
+ | **In-App Purchase** | `PURCHASE_TYPE.INAPP` | ❌ Not needed | One-time purchases (premium features, remove ads, etc.) |
94
+ | **Subscription** | `PURCHASE_TYPE.SUBS` | ✅ **REQUIRED** | Recurring purchases (monthly/yearly subscriptions) |
95
+
96
+ **Key Rules:**
97
+ - ✅ **In-App Products**: Use `productType: PURCHASE_TYPE.INAPP`, no `planIdentifier` needed
98
+ - ✅ **Subscriptions**: Must use `productType: PURCHASE_TYPE.SUBS` AND `planIdentifier: "your-plan-id"`
99
+ - ❌ **Missing planIdentifier** for subscriptions will cause purchase failures
100
+
87
101
  ### Complete Example: Get Product Info and Purchase
88
102
 
89
- Here's a complete example showing how to get product information and make a purchase:
103
+ Here's a complete example showing how to get product information and make purchases for both in-app products and subscriptions:
90
104
 
91
105
  ```typescript
92
- import { NativePurchases } from '@capgo/native-purchases';
106
+ import { NativePurchases, PURCHASE_TYPE } from '@capgo/native-purchases';
93
107
 
94
108
  class PurchaseManager {
95
- private productId = 'com.yourapp.premium.monthly';
109
+ // In-app product (one-time purchase)
110
+ private premiumProductId = 'com.yourapp.premium_features';
111
+
112
+ // Subscription products (require planIdentifier)
113
+ private monthlySubId = 'com.yourapp.premium.monthly';
114
+ private monthlyPlanId = 'monthly-plan'; // Base plan ID from store
115
+
116
+ private yearlySubId = 'com.yourapp.premium.yearly';
117
+ private yearlyPlanId = 'yearly-plan'; // Base plan ID from store
96
118
 
97
119
  async initializeStore() {
98
120
  try {
@@ -103,72 +125,130 @@ class PurchaseManager {
103
125
  }
104
126
 
105
127
  // 2. Get product information (REQUIRED by Apple - no hardcoded prices!)
106
- const product = await this.getProductInfo();
107
-
108
- // 3. Display product with dynamic info from store
109
- this.displayProduct(product);
128
+ await this.loadProducts();
110
129
 
111
130
  } catch (error) {
112
131
  console.error('Store initialization failed:', error);
113
132
  }
114
133
  }
115
134
 
116
- async getProductInfo() {
135
+ async loadProducts() {
117
136
  try {
118
- const { product } = await NativePurchases.getProduct({
119
- productIdentifier: this.productId
137
+ // Load in-app products
138
+ const { product: premiumProduct } = await NativePurchases.getProduct({
139
+ productIdentifier: this.premiumProductId,
140
+ productType: PURCHASE_TYPE.INAPP
120
141
  });
121
142
 
122
- console.log('Product loaded:', {
123
- id: product.identifier,
124
- title: product.title, // Use this for display (required by Apple)
125
- price: product.priceString, // Use this for display (required by Apple)
126
- description: product.description
143
+ // Load subscription products
144
+ const { products: subscriptions } = await NativePurchases.getProducts({
145
+ productIdentifiers: [this.monthlySubId, this.yearlySubId],
146
+ productType: PURCHASE_TYPE.SUBS
127
147
  });
128
148
 
129
- return product;
149
+ console.log('Products loaded:', {
150
+ premium: premiumProduct,
151
+ subscriptions: subscriptions
152
+ });
153
+
154
+ // Display products with dynamic info from store
155
+ this.displayProducts(premiumProduct, subscriptions);
156
+
130
157
  } catch (error) {
131
- console.error('Failed to get product:', error);
158
+ console.error('Failed to load products:', error);
132
159
  throw error;
133
160
  }
134
161
  }
135
162
 
136
- displayProduct(product: any) {
163
+ displayProducts(premiumProduct: any, subscriptions: any[]) {
137
164
  // ✅ CORRECT: Use dynamic product info (required by Apple)
138
- document.getElementById('product-title')!.textContent = product.title;
139
- document.getElementById('product-price')!.textContent = product.priceString;
140
- document.getElementById('product-description')!.textContent = product.description;
165
+
166
+ // Display one-time purchase
167
+ document.getElementById('premium-title')!.textContent = premiumProduct.title;
168
+ document.getElementById('premium-price')!.textContent = premiumProduct.priceString;
169
+
170
+ // Display subscriptions
171
+ subscriptions.forEach(sub => {
172
+ const element = document.getElementById(`sub-${sub.identifier}`);
173
+ if (element) {
174
+ element.textContent = `${sub.title} - ${sub.priceString}`;
175
+ }
176
+ });
141
177
 
142
178
  // ❌ WRONG: Never hardcode prices - Apple will reject your app
143
- // document.getElementById('product-price')!.textContent = '$9.99/month';
179
+ // document.getElementById('premium-price')!.textContent = '$9.99';
144
180
  }
145
181
 
146
- async purchaseProduct() {
182
+ // Purchase one-time product (no planIdentifier needed)
183
+ async purchaseInAppProduct() {
147
184
  try {
148
- console.log('Starting purchase...');
185
+ console.log('Starting in-app purchase...');
149
186
 
150
187
  const result = await NativePurchases.purchaseProduct({
151
- productIdentifier: this.productId,
188
+ productIdentifier: this.premiumProductId,
189
+ productType: PURCHASE_TYPE.INAPP,
152
190
  quantity: 1
153
191
  });
154
192
 
155
- console.log('Purchase successful!', result.transactionId);
193
+ console.log('In-app purchase successful!', result.transactionId);
194
+ await this.handleSuccessfulPurchase(result.transactionId, 'premium');
195
+
196
+ } catch (error) {
197
+ console.error('In-app purchase failed:', error);
198
+ this.handlePurchaseError(error);
199
+ }
200
+ }
201
+
202
+ // Purchase subscription (planIdentifier REQUIRED)
203
+ async purchaseMonthlySubscription() {
204
+ try {
205
+ console.log('Starting subscription purchase...');
206
+
207
+ const result = await NativePurchases.purchaseProduct({
208
+ productIdentifier: this.monthlySubId,
209
+ planIdentifier: this.monthlyPlanId, // REQUIRED for subscriptions
210
+ productType: PURCHASE_TYPE.SUBS, // REQUIRED for subscriptions
211
+ quantity: 1
212
+ });
156
213
 
157
- // Handle successful purchase
158
- await this.handleSuccessfulPurchase(result.transactionId);
214
+ console.log('Subscription purchase successful!', result.transactionId);
215
+ await this.handleSuccessfulPurchase(result.transactionId, 'monthly');
159
216
 
160
217
  } catch (error) {
161
- console.error('Purchase failed:', error);
218
+ console.error('Subscription purchase failed:', error);
219
+ this.handlePurchaseError(error);
220
+ }
221
+ }
222
+
223
+ // Purchase yearly subscription (planIdentifier REQUIRED)
224
+ async purchaseYearlySubscription() {
225
+ try {
226
+ console.log('Starting yearly subscription purchase...');
227
+
228
+ const result = await NativePurchases.purchaseProduct({
229
+ productIdentifier: this.yearlySubId,
230
+ planIdentifier: this.yearlyPlanId, // REQUIRED for subscriptions
231
+ productType: PURCHASE_TYPE.SUBS, // REQUIRED for subscriptions
232
+ quantity: 1
233
+ });
234
+
235
+ console.log('Yearly subscription successful!', result.transactionId);
236
+ await this.handleSuccessfulPurchase(result.transactionId, 'yearly');
237
+
238
+ } catch (error) {
239
+ console.error('Yearly subscription failed:', error);
162
240
  this.handlePurchaseError(error);
163
241
  }
164
242
  }
165
243
 
166
- async handleSuccessfulPurchase(transactionId: string) {
244
+ async handleSuccessfulPurchase(transactionId: string, purchaseType: string) {
167
245
  // 1. Grant access to premium features
168
246
  localStorage.setItem('premium_active', 'true');
247
+ localStorage.setItem('purchase_type', purchaseType);
169
248
 
170
249
  // 2. Update UI
171
- document.getElementById('subscription-status')!.textContent = 'Premium Active';
250
+ const statusText = purchaseType === 'premium' ? 'Premium Unlocked' : `${purchaseType} Subscription Active`;
251
+ document.getElementById('subscription-status')!.textContent = statusText;
172
252
 
173
253
  // 3. Optional: Verify purchase on your server
174
254
  await this.verifyPurchaseOnServer(transactionId);
@@ -223,8 +303,16 @@ const purchaseManager = new PurchaseManager();
223
303
  purchaseManager.initializeStore();
224
304
 
225
305
  // Attach to UI buttons
226
- document.getElementById('buy-button')?.addEventListener('click', () => {
227
- purchaseManager.purchaseProduct();
306
+ document.getElementById('buy-premium-button')?.addEventListener('click', () => {
307
+ purchaseManager.purchaseInAppProduct();
308
+ });
309
+
310
+ document.getElementById('buy-monthly-button')?.addEventListener('click', () => {
311
+ purchaseManager.purchaseMonthlySubscription();
312
+ });
313
+
314
+ document.getElementById('buy-yearly-button')?.addEventListener('click', () => {
315
+ purchaseManager.purchaseYearlySubscription();
228
316
  });
229
317
 
230
318
  document.getElementById('restore-button')?.addEventListener('click', () => {
@@ -237,15 +325,39 @@ document.getElementById('restore-button')?.addEventListener('click', () => {
237
325
  #### Get Multiple Products
238
326
 
239
327
  ```typescript
240
- // Get multiple products at once
241
- const getProducts = async () => {
328
+ import { NativePurchases, PURCHASE_TYPE } from '@capgo/native-purchases';
329
+
330
+ // Get in-app products (one-time purchases)
331
+ const getInAppProducts = async () => {
332
+ try {
333
+ const { products } = await NativePurchases.getProducts({
334
+ productIdentifiers: [
335
+ 'com.yourapp.premium_features',
336
+ 'com.yourapp.remove_ads',
337
+ 'com.yourapp.extra_content'
338
+ ],
339
+ productType: PURCHASE_TYPE.INAPP
340
+ });
341
+
342
+ products.forEach(product => {
343
+ console.log(`${product.title}: ${product.priceString}`);
344
+ });
345
+
346
+ return products;
347
+ } catch (error) {
348
+ console.error('Error getting in-app products:', error);
349
+ }
350
+ };
351
+
352
+ // Get subscription products
353
+ const getSubscriptions = async () => {
242
354
  try {
243
355
  const { products } = await NativePurchases.getProducts({
244
356
  productIdentifiers: [
245
357
  'com.yourapp.premium.monthly',
246
- 'com.yourapp.premium.yearly',
247
- 'com.yourapp.remove_ads'
248
- ]
358
+ 'com.yourapp.premium.yearly'
359
+ ],
360
+ productType: PURCHASE_TYPE.SUBS
249
361
  });
250
362
 
251
363
  products.forEach(product => {
@@ -254,7 +366,7 @@ const getProducts = async () => {
254
366
 
255
367
  return products;
256
368
  } catch (error) {
257
- console.error('Error getting products:', error);
369
+ console.error('Error getting subscriptions:', error);
258
370
  }
259
371
  };
260
372
  ```
@@ -262,8 +374,10 @@ const getProducts = async () => {
262
374
  #### Simple Purchase Flow
263
375
 
264
376
  ```typescript
265
- // Simple one-function purchase
266
- const buyPremium = async () => {
377
+ import { NativePurchases, PURCHASE_TYPE } from '@capgo/native-purchases';
378
+
379
+ // Simple one-time purchase (in-app product)
380
+ const buyInAppProduct = async () => {
267
381
  try {
268
382
  // Check billing support
269
383
  const { isBillingSupported } = await NativePurchases.isBillingSupported();
@@ -274,16 +388,18 @@ const buyPremium = async () => {
274
388
 
275
389
  // Get product (for price display)
276
390
  const { product } = await NativePurchases.getProduct({
277
- productIdentifier: 'com.yourapp.premium'
391
+ productIdentifier: 'com.yourapp.premium_features',
392
+ productType: PURCHASE_TYPE.INAPP
278
393
  });
279
394
 
280
395
  // Confirm with user (showing real price from store)
281
396
  const confirmed = confirm(`Purchase ${product.title} for ${product.priceString}?`);
282
397
  if (!confirmed) return;
283
398
 
284
- // Make purchase
399
+ // Make purchase (no planIdentifier needed for in-app)
285
400
  const result = await NativePurchases.purchaseProduct({
286
- productIdentifier: 'com.yourapp.premium',
401
+ productIdentifier: 'com.yourapp.premium_features',
402
+ productType: PURCHASE_TYPE.INAPP,
287
403
  quantity: 1
288
404
  });
289
405
 
@@ -293,6 +409,41 @@ const buyPremium = async () => {
293
409
  alert('Purchase failed: ' + error.message);
294
410
  }
295
411
  };
412
+
413
+ // Simple subscription purchase (requires planIdentifier)
414
+ const buySubscription = async () => {
415
+ try {
416
+ // Check billing support
417
+ const { isBillingSupported } = await NativePurchases.isBillingSupported();
418
+ if (!isBillingSupported) {
419
+ alert('Purchases not supported on this device');
420
+ return;
421
+ }
422
+
423
+ // Get subscription product (for price display)
424
+ const { product } = await NativePurchases.getProduct({
425
+ productIdentifier: 'com.yourapp.premium.monthly',
426
+ productType: PURCHASE_TYPE.SUBS
427
+ });
428
+
429
+ // Confirm with user (showing real price from store)
430
+ const confirmed = confirm(`Subscribe to ${product.title} for ${product.priceString}?`);
431
+ if (!confirmed) return;
432
+
433
+ // Make subscription purchase (planIdentifier REQUIRED)
434
+ const result = await NativePurchases.purchaseProduct({
435
+ productIdentifier: 'com.yourapp.premium.monthly',
436
+ planIdentifier: 'monthly-plan', // REQUIRED for subscriptions
437
+ productType: PURCHASE_TYPE.SUBS, // REQUIRED for subscriptions
438
+ quantity: 1
439
+ });
440
+
441
+ alert('Subscription successful! Transaction ID: ' + result.transactionId);
442
+
443
+ } catch (error) {
444
+ alert('Subscription failed: ' + error.message);
445
+ }
446
+ };
296
447
  ```
297
448
 
298
449
  ### Check if billing is supported
@@ -366,13 +517,14 @@ import axios from 'axios'; // Make sure to install axios: npm install axios
366
517
  class Store {
367
518
  // ... (previous code remains the same)
368
519
 
520
+ // Purchase in-app product
369
521
  async purchaseProduct(productId: string) {
370
522
  try {
371
523
  const transaction = await NativePurchases.purchaseProduct({
372
524
  productIdentifier: productId,
373
525
  productType: PURCHASE_TYPE.INAPP
374
526
  });
375
- console.log('Purchase successful:', transaction);
527
+ console.log('In-app purchase successful:', transaction);
376
528
 
377
529
  // Immediately grant access to the purchased content
378
530
  await this.grantAccess(productId);
@@ -387,6 +539,29 @@ class Store {
387
539
  }
388
540
  }
389
541
 
542
+ // Purchase subscription (requires planIdentifier)
543
+ async purchaseSubscription(productId: string, planId: string) {
544
+ try {
545
+ const transaction = await NativePurchases.purchaseProduct({
546
+ productIdentifier: productId,
547
+ planIdentifier: planId, // REQUIRED for subscriptions
548
+ productType: PURCHASE_TYPE.SUBS // REQUIRED for subscriptions
549
+ });
550
+ console.log('Subscription purchase successful:', transaction);
551
+
552
+ // Immediately grant access to the subscription content
553
+ await this.grantAccess(productId);
554
+
555
+ // Initiate server-side validation asynchronously
556
+ this.validatePurchaseOnServer(transaction).catch(console.error);
557
+
558
+ return transaction;
559
+ } catch (error) {
560
+ console.error('Subscription purchase failed:', error);
561
+ throw error;
562
+ }
563
+ }
564
+
390
565
  private async grantAccess(productId: string) {
391
566
  // Implement logic to grant immediate access to the purchased content
392
567
  console.log(`Granting access to ${productId}`);
@@ -411,13 +586,18 @@ class Store {
411
586
  }
412
587
  }
413
588
 
414
- // Usage remains the same
589
+ // Usage examples
415
590
  const store = new Store();
416
591
  await store.initialize();
417
592
 
418
593
  try {
419
- await store.purchaseProduct('premium_subscription');
420
- console.log('Purchase completed successfully');
594
+ // Purchase in-app product (one-time purchase)
595
+ await store.purchaseProduct('premium_features');
596
+ console.log('In-app purchase completed successfully');
597
+
598
+ // Purchase subscription (requires planIdentifier)
599
+ await store.purchaseSubscription('premium_monthly', 'monthly-plan');
600
+ console.log('Subscription completed successfully');
421
601
  } catch (error) {
422
602
  console.error('Purchase failed:', error);
423
603
  }
@@ -375,15 +375,14 @@ public class NativePurchasesPlugin extends Plugin {
375
375
  return;
376
376
  }
377
377
 
378
- String productId = productType.equals("inapp")
379
- ? productIdentifier
380
- : planIdentifier;
381
- Log.d(TAG, "Using product ID for query: " + productId);
378
+ // For subscriptions, always use the productIdentifier (subscription ID) to query
379
+ // The planIdentifier is used later when setting the offer token
380
+ Log.d(TAG, "Using product ID for query: " + productIdentifier);
382
381
 
383
382
  ImmutableList<QueryProductDetailsParams.Product> productList =
384
383
  ImmutableList.of(
385
384
  QueryProductDetailsParams.Product.newBuilder()
386
- .setProductId(productId)
385
+ .setProductId(productIdentifier)
387
386
  .setProductType(
388
387
  productType.equals("inapp")
389
388
  ? BillingClient.ProductType.INAPP
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/native-purchases",
3
- "version": "7.1.29",
3
+ "version": "7.1.31",
4
4
  "description": "In-app Subscriptions Made Easy",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",