@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/
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
135
|
+
async loadProducts() {
|
|
117
136
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
|
158
|
+
console.error('Failed to load products:', error);
|
|
132
159
|
throw error;
|
|
133
160
|
}
|
|
134
161
|
}
|
|
135
162
|
|
|
136
|
-
|
|
163
|
+
displayProducts(premiumProduct: any, subscriptions: any[]) {
|
|
137
164
|
// ✅ CORRECT: Use dynamic product info (required by Apple)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
document.getElementById('
|
|
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('
|
|
179
|
+
// document.getElementById('premium-price')!.textContent = '$9.99';
|
|
144
180
|
}
|
|
145
181
|
|
|
146
|
-
|
|
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.
|
|
188
|
+
productIdentifier: this.premiumProductId,
|
|
189
|
+
productType: PURCHASE_TYPE.INAPP,
|
|
152
190
|
quantity: 1
|
|
153
191
|
});
|
|
154
192
|
|
|
155
|
-
console.log('
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
266
|
-
|
|
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.
|
|
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.
|
|
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('
|
|
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
|
|
589
|
+
// Usage examples
|
|
415
590
|
const store = new Store();
|
|
416
591
|
await store.initialize();
|
|
417
592
|
|
|
418
593
|
try {
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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(
|
|
385
|
+
.setProductId(productIdentifier)
|
|
387
386
|
.setProductType(
|
|
388
387
|
productType.equals("inapp")
|
|
389
388
|
? BillingClient.ProductType.INAPP
|