@capgo/native-purchases 7.18.0-alpha.0 → 7.18.0
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 +293 -48
- package/android/build.gradle +2 -2
- package/android/src/main/java/ee/forgr/nativepurchases/NativePurchasesPlugin.java +148 -77
- package/dist/docs.json +37 -4
- package/dist/esm/definitions.d.ts +76 -0
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/NativePurchasesPlugin/NativePurchasesPlugin.swift +204 -304
- package/ios/Sources/NativePurchasesPlugin/Product+CapacitorPurchasesPlugin.swift +0 -1
- package/ios/Sources/NativePurchasesPlugin/TransactionHelpers.swift +85 -129
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,6 +28,17 @@ Perfect for apps monetizing through one-time purchases or recurring subscription
|
|
|
28
28
|
|
|
29
29
|
The most complete doc is available here: https://capgo.app/docs/plugins/native-purchases/
|
|
30
30
|
|
|
31
|
+
## Compatibility
|
|
32
|
+
|
|
33
|
+
| Plugin version | Capacitor compatibility | Maintained |
|
|
34
|
+
| -------------- | ----------------------- | ---------- |
|
|
35
|
+
| v8.\*.\* | v8.\*.\* | ✅ |
|
|
36
|
+
| v7.\*.\* | v7.\*.\* | On demand |
|
|
37
|
+
| v6.\*.\* | v6.\*.\* | ❌ |
|
|
38
|
+
| v5.\*.\* | v5.\*.\* | ❌ |
|
|
39
|
+
|
|
40
|
+
> **Note:** The major version of this plugin follows the major version of Capacitor. Use the version that matches your Capacitor installation (e.g., plugin v8 for Capacitor 8). Only the latest major version is actively maintained.
|
|
41
|
+
|
|
31
42
|
## Install
|
|
32
43
|
|
|
33
44
|
```bash
|
|
@@ -169,6 +180,8 @@ class PurchaseManager {
|
|
|
169
180
|
productIdentifiers: [this.monthlySubId, this.yearlySubId],
|
|
170
181
|
productType: PURCHASE_TYPE.SUBS
|
|
171
182
|
});
|
|
183
|
+
// Android note: subscriptions can include multiple entries per product (one per offer/base plan).
|
|
184
|
+
// Use `identifier` (base plan), `offerToken`, and optional `offerId` to pick a specific offer.
|
|
172
185
|
|
|
173
186
|
console.log('Products loaded:', {
|
|
174
187
|
premium: premiumProduct,
|
|
@@ -447,12 +460,24 @@ const buyInAppProduct = async () => {
|
|
|
447
460
|
|
|
448
461
|
alert('Purchase successful! Transaction ID: ' + result.transactionId);
|
|
449
462
|
|
|
450
|
-
//
|
|
463
|
+
// Access the full receipt data for backend validation
|
|
451
464
|
if (result.receipt) {
|
|
452
|
-
//
|
|
465
|
+
// iOS: Base64-encoded StoreKit receipt - send this to your backend
|
|
466
|
+
console.log('iOS Receipt (base64):', result.receipt);
|
|
453
467
|
await validateReceipt(result.receipt);
|
|
454
468
|
}
|
|
455
469
|
|
|
470
|
+
if (result.jwsRepresentation) {
|
|
471
|
+
// iOS: StoreKit 2 JWS representation - alternative to receipt
|
|
472
|
+
console.log('iOS JWS:', result.jwsRepresentation);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (result.purchaseToken) {
|
|
476
|
+
// Android: Purchase token - send this to your backend
|
|
477
|
+
console.log('Android Purchase Token:', result.purchaseToken);
|
|
478
|
+
await validatePurchaseToken(result.purchaseToken, result.productIdentifier);
|
|
479
|
+
}
|
|
480
|
+
|
|
456
481
|
} catch (error) {
|
|
457
482
|
alert('Purchase failed: ' + error.message);
|
|
458
483
|
}
|
|
@@ -493,12 +518,24 @@ const buySubscription = async () => {
|
|
|
493
518
|
|
|
494
519
|
alert('Subscription successful! Transaction ID: ' + result.transactionId);
|
|
495
520
|
|
|
496
|
-
//
|
|
521
|
+
// Access the full receipt data for backend validation
|
|
497
522
|
if (result.receipt) {
|
|
498
|
-
//
|
|
523
|
+
// iOS: Base64-encoded StoreKit receipt - send this to your backend
|
|
524
|
+
console.log('iOS Receipt (base64):', result.receipt);
|
|
499
525
|
await validateReceipt(result.receipt);
|
|
500
526
|
}
|
|
501
527
|
|
|
528
|
+
if (result.jwsRepresentation) {
|
|
529
|
+
// iOS: StoreKit 2 JWS representation - alternative to receipt
|
|
530
|
+
console.log('iOS JWS:', result.jwsRepresentation);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (result.purchaseToken) {
|
|
534
|
+
// Android: Purchase token - send this to your backend
|
|
535
|
+
console.log('Android Purchase Token:', result.purchaseToken);
|
|
536
|
+
await validatePurchaseToken(result.purchaseToken, result.productIdentifier);
|
|
537
|
+
}
|
|
538
|
+
|
|
502
539
|
} catch (error) {
|
|
503
540
|
alert('Subscription failed: ' + error.message);
|
|
504
541
|
}
|
|
@@ -1216,12 +1253,65 @@ await NativePurchases.getPluginVersion();
|
|
|
1216
1253
|
|
|
1217
1254
|
## Backend Validation
|
|
1218
1255
|
|
|
1219
|
-
|
|
1256
|
+
### ✅ Full Receipt Data Access
|
|
1257
|
+
|
|
1258
|
+
**This plugin provides complete access to verified receipt data for server-side validation.** You get all the information needed to validate purchases with Apple and Google servers.
|
|
1259
|
+
|
|
1260
|
+
**For iOS:**
|
|
1261
|
+
- ✅ `transaction.receipt` - Complete base64-encoded StoreKit receipt (for Apple's receipt verification API)
|
|
1262
|
+
- ✅ `transaction.jwsRepresentation` - StoreKit 2 JSON Web Signature (for App Store Server API v2)
|
|
1263
|
+
|
|
1264
|
+
**For Android:**
|
|
1265
|
+
- ✅ `transaction.purchaseToken` - Google Play purchase token (for Google Play Developer API)
|
|
1266
|
+
- ✅ `transaction.orderId` - Google Play order identifier
|
|
1267
|
+
|
|
1268
|
+
These fields contain the **full verified receipt payload** that you can send directly to your backend for validation with Apple's and Google's servers.
|
|
1269
|
+
|
|
1270
|
+
#### Migrating from cordova-plugin-purchase?
|
|
1271
|
+
|
|
1272
|
+
If you're coming from cordova-plugin-purchase, here's the mapping:
|
|
1273
|
+
|
|
1274
|
+
| cordova-plugin-purchase | @capgo/native-purchases | Platform | Notes |
|
|
1275
|
+
|-------------------------|-------------------------|----------|-------|
|
|
1276
|
+
| `transaction.transactionReceipt` | `transaction.receipt` (base64) | iOS | Legacy StoreKit receipt format (same value as Cordova) |
|
|
1277
|
+
| — | `transaction.jwsRepresentation` (JWS) | iOS | StoreKit 2 JWS format (iOS 15+, additional field with no Cordova equivalent; Apple's recommended modern format for new implementations) |
|
|
1278
|
+
| `transaction.purchaseToken` | `transaction.purchaseToken` | Android | Same field name |
|
|
1279
|
+
|
|
1280
|
+
**This plugin already exposes everything you need for backend verification!** The `receipt` and `purchaseToken` fields contain the complete verified receipt data, and `jwsRepresentation` provides an additional StoreKit 2 representation when available.
|
|
1281
|
+
|
|
1282
|
+
**Note:** On iOS, `jwsRepresentation` is only available for StoreKit 2 transactions (iOS 15+) and is Apple's recommended modern format. For maximum compatibility, use `receipt` which works on all iOS versions; when available, you can also send `jwsRepresentation` to backends that support App Store Server API v2.
|
|
1220
1283
|
|
|
1221
|
-
|
|
1284
|
+
### Why Backend Validation?
|
|
1285
|
+
|
|
1286
|
+
It's crucial to validate receipts on your server to ensure the integrity of purchases. Client-side data can be manipulated, but server-side validation with Apple/Google servers ensures purchases are legitimate.
|
|
1287
|
+
|
|
1288
|
+
### Receipt Data Available for Backend Verification
|
|
1289
|
+
|
|
1290
|
+
The `Transaction` object returned by `purchaseProduct()`, `getPurchases()`, and `restorePurchases()` includes all data needed for server-side validation:
|
|
1291
|
+
|
|
1292
|
+
**iOS Receipt Data:**
|
|
1293
|
+
- **`receipt`** - Base64-encoded StoreKit receipt (legacy format, works with Apple's receipt verification API)
|
|
1294
|
+
- **`jwsRepresentation`** - JSON Web Signature for StoreKit 2 (recommended for new implementations, works with App Store Server API)
|
|
1295
|
+
- **`transactionId`** - Unique transaction identifier
|
|
1296
|
+
|
|
1297
|
+
**Android Receipt Data:**
|
|
1298
|
+
- **`purchaseToken`** - Google Play purchase token (required for server-side validation)
|
|
1299
|
+
- **`orderId`** - Google Play order identifier
|
|
1300
|
+
- **`transactionId`** - Alias for purchaseToken
|
|
1301
|
+
|
|
1302
|
+
**All platforms include:**
|
|
1303
|
+
- `productIdentifier` - The product that was purchased
|
|
1304
|
+
- `purchaseDate` - When the purchase occurred
|
|
1305
|
+
- Additional metadata like `appAccountToken`, `quantity`, etc.
|
|
1306
|
+
|
|
1307
|
+
### Complete Backend Validation Example
|
|
1308
|
+
|
|
1309
|
+
#### Cloudflare Worker Setup
|
|
1222
1310
|
Create a new Cloudflare Worker and follow the instructions in folder (`validator`)[/validator/README.md]
|
|
1223
1311
|
|
|
1224
|
-
|
|
1312
|
+
#### Client-Side Implementation
|
|
1313
|
+
|
|
1314
|
+
Here's how to access the receipt data and send it to your backend for validation:
|
|
1225
1315
|
|
|
1226
1316
|
```typescript
|
|
1227
1317
|
import { Capacitor } from '@capacitor/core';
|
|
@@ -1284,18 +1374,40 @@ class Store {
|
|
|
1284
1374
|
|
|
1285
1375
|
private async validatePurchaseOnServer(transaction: Transaction) {
|
|
1286
1376
|
const serverUrl = 'https://your-server-url.com/validate-purchase';
|
|
1377
|
+
const platform = Capacitor.getPlatform();
|
|
1378
|
+
|
|
1287
1379
|
try {
|
|
1380
|
+
// Prepare receipt data based on platform
|
|
1381
|
+
const receiptData = platform === 'ios'
|
|
1382
|
+
? {
|
|
1383
|
+
// iOS: Send the full receipt (base64 encoded) or JWS representation
|
|
1384
|
+
receipt: transaction.receipt, // StoreKit receipt (base64)
|
|
1385
|
+
jwsRepresentation: transaction.jwsRepresentation, // StoreKit 2 JWS (optional, recommended for new apps)
|
|
1386
|
+
transactionId: transaction.transactionId,
|
|
1387
|
+
platform: 'ios'
|
|
1388
|
+
}
|
|
1389
|
+
: {
|
|
1390
|
+
// Android: Send the purchase token and order ID
|
|
1391
|
+
purchaseToken: transaction.purchaseToken, // Required for Google Play validation
|
|
1392
|
+
orderId: transaction.orderId, // Google Play order ID
|
|
1393
|
+
transactionId: transaction.transactionId,
|
|
1394
|
+
platform: 'android'
|
|
1395
|
+
};
|
|
1396
|
+
|
|
1288
1397
|
const response = await axios.post(serverUrl, {
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1398
|
+
...receiptData,
|
|
1399
|
+
productId: transaction.productIdentifier,
|
|
1400
|
+
purchaseDate: transaction.purchaseDate,
|
|
1401
|
+
// Include user ID or other app-specific data
|
|
1402
|
+
userId: 'your-user-id'
|
|
1292
1403
|
});
|
|
1293
1404
|
|
|
1294
1405
|
console.log('Server validation response:', response.data);
|
|
1295
|
-
|
|
1406
|
+
return response.data;
|
|
1296
1407
|
} catch (error) {
|
|
1297
1408
|
console.error('Error in server-side validation:', error);
|
|
1298
1409
|
// Implement retry logic or notify the user if necessary
|
|
1410
|
+
throw error;
|
|
1299
1411
|
}
|
|
1300
1412
|
}
|
|
1301
1413
|
}
|
|
@@ -1317,7 +1429,7 @@ try {
|
|
|
1317
1429
|
}
|
|
1318
1430
|
```
|
|
1319
1431
|
|
|
1320
|
-
Now, let's look at how the server-side (Node.js) code
|
|
1432
|
+
Now, let's look at how the server-side (Node.js) code handles the validation:
|
|
1321
1433
|
|
|
1322
1434
|
```typescript
|
|
1323
1435
|
import express from 'express';
|
|
@@ -1329,49 +1441,179 @@ app.use(express.json());
|
|
|
1329
1441
|
const CLOUDFLARE_WORKER_URL = 'https://your-cloudflare-worker-url.workers.dev';
|
|
1330
1442
|
|
|
1331
1443
|
app.post('/validate-purchase', async (req, res) => {
|
|
1332
|
-
const {
|
|
1444
|
+
const { platform, receipt, jwsRepresentation, purchaseToken, productId, userId } = req.body;
|
|
1333
1445
|
|
|
1334
1446
|
try {
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1447
|
+
let validationResponse;
|
|
1448
|
+
|
|
1449
|
+
if (platform === 'ios') {
|
|
1450
|
+
// iOS: Validate using receipt or JWS representation
|
|
1451
|
+
if (!receipt && !jwsRepresentation) {
|
|
1452
|
+
return res.status(400).json({
|
|
1453
|
+
success: false,
|
|
1454
|
+
error: 'Missing receipt data: either receipt or jwsRepresentation is required for iOS'
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// Option 1: Use legacy receipt validation (recommended for compatibility)
|
|
1459
|
+
if (receipt) {
|
|
1460
|
+
validationResponse = await axios.post(`${CLOUDFLARE_WORKER_URL}/apple`, {
|
|
1461
|
+
receipt: receipt, // Base64-encoded receipt from transaction.receipt
|
|
1462
|
+
password: 'your-app-shared-secret' // App-Specific Shared Secret from App Store Connect (required for auto-renewable subscriptions)
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
// Option 2: Use StoreKit 2 App Store Server API (recommended for new implementations)
|
|
1466
|
+
else if (jwsRepresentation) {
|
|
1467
|
+
// Validate JWS token with App Store Server API
|
|
1468
|
+
// Note: JWS verification requires decoding and validating the signature
|
|
1469
|
+
// Implementation depends on your backend setup - see Apple's documentation:
|
|
1470
|
+
// https://developer.apple.com/documentation/appstoreserverapi/jwstransaction
|
|
1471
|
+
validationResponse = await axios.post(`${CLOUDFLARE_WORKER_URL}/apple-jws`, {
|
|
1472
|
+
jws: jwsRepresentation
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
} else if (platform === 'android') {
|
|
1476
|
+
// Android: Validate using purchase token with Google Play Developer API
|
|
1477
|
+
if (!purchaseToken) {
|
|
1478
|
+
return res.status(400).json({
|
|
1479
|
+
success: false,
|
|
1480
|
+
error: 'Missing purchaseToken for Android validation'
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
validationResponse = await axios.post(`${CLOUDFLARE_WORKER_URL}/google`, {
|
|
1485
|
+
purchaseToken: purchaseToken, // From transaction.purchaseToken
|
|
1486
|
+
productId: productId,
|
|
1487
|
+
packageName: 'com.yourapp.package'
|
|
1488
|
+
});
|
|
1489
|
+
} else {
|
|
1490
|
+
return res.status(400).json({
|
|
1491
|
+
success: false,
|
|
1492
|
+
error: 'Invalid platform'
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1339
1495
|
|
|
1340
1496
|
const validationResult = validationResponse.data;
|
|
1341
1497
|
|
|
1342
1498
|
// Process the validation result
|
|
1343
1499
|
if (validationResult.isValid) {
|
|
1344
1500
|
// Update user status in the database
|
|
1345
|
-
|
|
1501
|
+
await updateUserPurchase(userId, {
|
|
1502
|
+
productId,
|
|
1503
|
+
platform,
|
|
1504
|
+
transactionId: req.body.transactionId,
|
|
1505
|
+
validated: true,
|
|
1506
|
+
validatedAt: new Date(),
|
|
1507
|
+
receiptData: validationResult
|
|
1508
|
+
});
|
|
1346
1509
|
|
|
1347
|
-
|
|
1348
|
-
console.log(`Purchase validated for transaction ${transactionId}`);
|
|
1510
|
+
console.log(`Purchase validated for user ${userId}, product ${productId}`);
|
|
1349
1511
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1512
|
+
res.json({
|
|
1513
|
+
success: true,
|
|
1514
|
+
validated: true,
|
|
1515
|
+
message: 'Purchase successfully validated'
|
|
1516
|
+
});
|
|
1352
1517
|
} else {
|
|
1353
1518
|
// Handle invalid purchase
|
|
1354
|
-
console.warn(`Invalid purchase detected for
|
|
1355
|
-
|
|
1356
|
-
//
|
|
1519
|
+
console.warn(`Invalid purchase detected for user ${userId}`);
|
|
1520
|
+
|
|
1521
|
+
// Flag for investigation but don't block the user immediately
|
|
1522
|
+
await flagSuspiciousPurchase(userId, req.body);
|
|
1523
|
+
|
|
1524
|
+
res.json({
|
|
1525
|
+
success: true, // Don't block the user
|
|
1526
|
+
validated: false,
|
|
1527
|
+
message: 'Purchase validation pending review'
|
|
1528
|
+
});
|
|
1357
1529
|
}
|
|
1358
1530
|
|
|
1359
|
-
// Always respond with a success to the app
|
|
1360
|
-
// This ensures the app doesn't block the user's access
|
|
1361
|
-
res.json({ success: true });
|
|
1362
1531
|
} catch (error) {
|
|
1363
1532
|
console.error('Error validating purchase:', error);
|
|
1533
|
+
|
|
1534
|
+
// Log the error for investigation
|
|
1535
|
+
await logValidationError(userId, req.body, error);
|
|
1536
|
+
|
|
1364
1537
|
// Still respond with success to the app
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1538
|
+
// This ensures the app doesn't block the user's access
|
|
1539
|
+
res.json({
|
|
1540
|
+
success: true,
|
|
1541
|
+
validated: 'pending',
|
|
1542
|
+
message: 'Validation will be retried'
|
|
1543
|
+
});
|
|
1368
1544
|
}
|
|
1369
1545
|
});
|
|
1370
1546
|
|
|
1547
|
+
// Helper function to update user purchase status
|
|
1548
|
+
async function updateUserPurchase(userId: string, purchaseData: any) {
|
|
1549
|
+
// Implement your database logic here
|
|
1550
|
+
console.log('Updating purchase for user:', userId);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// Helper function to flag suspicious purchases
|
|
1554
|
+
async function flagSuspiciousPurchase(userId: string, purchaseData: any) {
|
|
1555
|
+
// Implement your logic to flag and review suspicious purchases
|
|
1556
|
+
console.log('Flagging suspicious purchase:', userId);
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// Helper function to log validation errors
|
|
1560
|
+
async function logValidationError(userId: string, purchaseData: any, error: any) {
|
|
1561
|
+
// Implement your error logging logic
|
|
1562
|
+
console.log('Logging validation error:', userId, error);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1371
1565
|
// Start the server
|
|
1372
1566
|
app.listen(3000, () => console.log('Server running on port 3000'));
|
|
1373
1567
|
```
|
|
1374
1568
|
|
|
1569
|
+
### Alternative: Direct Store API Validation
|
|
1570
|
+
|
|
1571
|
+
Instead of using a Cloudflare Worker, you can validate directly with Apple and Google:
|
|
1572
|
+
|
|
1573
|
+
**iOS - Apple Receipt Verification API:**
|
|
1574
|
+
```typescript
|
|
1575
|
+
// Production: https://buy.itunes.apple.com/verifyReceipt
|
|
1576
|
+
// Sandbox: https://sandbox.itunes.apple.com/verifyReceipt
|
|
1577
|
+
|
|
1578
|
+
async function validateAppleReceipt(receiptData: string) {
|
|
1579
|
+
const response = await axios.post('https://buy.itunes.apple.com/verifyReceipt', {
|
|
1580
|
+
'receipt-data': receiptData,
|
|
1581
|
+
'password': 'your-shared-secret', // App-Specific Shared Secret from App Store Connect (required for auto-renewable subscriptions)
|
|
1582
|
+
'exclude-old-transactions': true
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1585
|
+
return response.data;
|
|
1586
|
+
}
|
|
1587
|
+
```
|
|
1588
|
+
|
|
1589
|
+
**Android - Google Play Developer API:**
|
|
1590
|
+
```typescript
|
|
1591
|
+
// Requires Google Play Developer API credentials
|
|
1592
|
+
// See: https://developers.google.com/android-publisher/getting_started
|
|
1593
|
+
|
|
1594
|
+
import { google } from 'googleapis';
|
|
1595
|
+
|
|
1596
|
+
async function validateGooglePurchase(packageName: string, productId: string, purchaseToken: string) {
|
|
1597
|
+
const androidPublisher = google.androidpublisher('v3');
|
|
1598
|
+
|
|
1599
|
+
const auth = new google.auth.GoogleAuth({
|
|
1600
|
+
keyFile: 'path/to/service-account-key.json',
|
|
1601
|
+
scopes: ['https://www.googleapis.com/auth/androidpublisher'],
|
|
1602
|
+
});
|
|
1603
|
+
|
|
1604
|
+
const authClient = await auth.getClient();
|
|
1605
|
+
|
|
1606
|
+
const response = await androidPublisher.purchases.products.get({
|
|
1607
|
+
auth: authClient,
|
|
1608
|
+
packageName: packageName,
|
|
1609
|
+
productId: productId,
|
|
1610
|
+
token: purchaseToken
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
return response.data;
|
|
1614
|
+
}
|
|
1615
|
+
```
|
|
1616
|
+
|
|
1375
1617
|
Key points about this approach:
|
|
1376
1618
|
|
|
1377
1619
|
1. The app immediately grants access after a successful purchase, ensuring a smooth user experience.
|
|
@@ -1776,8 +2018,8 @@ which is useful for determining if users are entitled to features from earlier b
|
|
|
1776
2018
|
| Prop | Type | Description | Default | Since |
|
|
1777
2019
|
| -------------------------- | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | ------ |
|
|
1778
2020
|
| **`transactionId`** | <code>string</code> | Unique identifier for the transaction. | | 1.0.0 |
|
|
1779
|
-
| **`receipt`** | <code>string</code> | Receipt data for validation (base64 encoded StoreKit receipt). Send this to your backend for server-side validation with Apple's receipt verification API. The receipt remains available even after refund - server validation is required to detect refunded transactions.
|
|
1780
|
-
| **`jwsRepresentation`** | <code>string</code> | StoreKit 2 JSON Web Signature (JWS) payload describing the verified transaction. Send this to your backend when using Apple's App Store Server API v2 instead of raw receipts. Only available when the transaction originated from StoreKit 2 APIs (e.g. <a href="#transaction">Transaction</a>.updates).
|
|
2021
|
+
| **`receipt`** | <code>string</code> | Receipt data for validation (base64 encoded StoreKit receipt). **This is the full verified receipt payload from Apple StoreKit.** Send this to your backend for server-side validation with Apple's receipt verification API. The receipt remains available even after refund - server validation is required to detect refunded transactions. **For backend validation:** - Use Apple's receipt verification API: https://buy.itunes.apple.com/verifyReceipt (production) - Or sandbox: https://sandbox.itunes.apple.com/verifyReceipt - This contains all transaction data needed for validation **Note:** Apple recommends migrating to App Store Server API v2 with `jwsRepresentation` for new implementations. The legacy receipt verification API continues to work but may be deprecated in the future. | | 1.0.0 |
|
|
2022
|
+
| **`jwsRepresentation`** | <code>string</code> | StoreKit 2 JSON Web Signature (JWS) payload describing the verified transaction. **This is the full verified receipt in JWS format (StoreKit 2).** Send this to your backend when using Apple's App Store Server API v2 instead of raw receipts. Only available when the transaction originated from StoreKit 2 APIs (e.g. <a href="#transaction">Transaction</a>.updates). **For backend validation:** - Use Apple's App Store Server API v2 to decode and verify the JWS - This is the modern alternative to the legacy receipt format - Contains signed transaction information from Apple | | 7.13.2 |
|
|
1781
2023
|
| **`appAccountToken`** | <code>string \| null</code> | An optional obfuscated identifier that uniquely associates the transaction with a user account in your app. PURPOSE: - Fraud detection: Helps platforms detect irregular activity (e.g., many devices purchasing on the same account) - User linking: Links purchases to in-game characters, avatars, or in-app profiles PLATFORM DIFFERENCES: - iOS: Must be a valid UUID format (e.g., "550e8400-e29b-41d4-a716-446655440000") Apple's StoreKit 2 requires UUID format for the appAccountToken parameter - Android: Can be any obfuscated string (max 64 chars), maps to Google Play's ObfuscatedAccountId Google recommends using encryption or one-way hash SECURITY REQUIREMENTS (especially for Android): - DO NOT store Personally Identifiable Information (PII) like emails in cleartext - Use encryption or a one-way hash to generate an obfuscated identifier - Maximum length: 64 characters (both platforms) - Storing PII in cleartext will result in purchases being blocked by Google Play IMPLEMENTATION EXAMPLE: ```typescript // For iOS: Generate a deterministic UUID from user ID import { v5 as uuidv5 } from 'uuid'; const NAMESPACE = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; // Your app's namespace UUID const appAccountToken = uuidv5(userId, NAMESPACE); // For Android: Can also use UUID or any hashed value // The same UUID approach works for both platforms ``` | | |
|
|
1782
2024
|
| **`productIdentifier`** | <code>string</code> | <a href="#product">Product</a> identifier associated with the transaction. | | 1.0.0 |
|
|
1783
2025
|
| **`purchaseDate`** | <code>string</code> | Purchase date of the transaction in ISO 8601 format. | | 1.0.0 |
|
|
@@ -1791,7 +2033,7 @@ which is useful for determining if users are entitled to features from earlier b
|
|
|
1791
2033
|
| **`subscriptionState`** | <code>'unknown' \| 'subscribed' \| 'expired' \| 'revoked' \| 'inGracePeriod' \| 'inBillingRetryPeriod'</code> | Current subscription state reported by StoreKit. Possible values: - `"subscribed"`: Auto-renewing and in good standing - `"expired"`: Lapsed with no access - `"revoked"`: Access removed due to refund or issue - `"inGracePeriod"`: Payment issue but still in grace access window - `"inBillingRetryPeriod"`: StoreKit retrying failed billing - `"unknown"`: StoreKit did not report a state | | 7.13.2 |
|
|
1792
2034
|
| **`purchaseState`** | <code>string</code> | Purchase state of the transaction (numeric string value). **Android Values:** - `"1"`: Purchase completed and valid (PURCHASED state) - `"0"`: Payment pending (PENDING state, e.g., cash payment processing) - Other numeric values: Various other states Always check `purchaseState === "1"` on Android to verify a valid purchase. Refunded purchases typically disappear from getPurchases() rather than showing a different state. | | 1.0.0 |
|
|
1793
2035
|
| **`orderId`** | <code>string</code> | Order ID associated with the transaction. Use this for server-side verification on Android. This is the Google Play order ID. | | 1.0.0 |
|
|
1794
|
-
| **`purchaseToken`** | <code>string</code> | Purchase token associated with the transaction. Send this to your backend for server-side validation with Google Play Developer API. This is the Android equivalent of iOS's receipt field.
|
|
2036
|
+
| **`purchaseToken`** | <code>string</code> | Purchase token associated with the transaction. **This is the full verified purchase token from Google Play.** Send this to your backend for server-side validation with Google Play Developer API. This is the Android equivalent of iOS's receipt field. **For backend validation:** - Use Google Play Developer API v3 to verify the purchase - API endpoint: androidpublisher.purchases.products.get() or purchases.subscriptions.get() - This token contains all data needed for validation with Google servers - Can also be used for subscription status checks and cancellation detection | | 1.0.0 |
|
|
1795
2037
|
| **`isAcknowledged`** | <code>boolean</code> | Whether the purchase has been acknowledged. Purchases must be acknowledged within 3 days or they will be refunded. By default, this plugin automatically acknowledges purchases unless you set `autoAcknowledgePurchases: false` in purchaseProduct(). | | 1.0.0 |
|
|
1796
2038
|
| **`quantity`** | <code>number</code> | Quantity purchased. | <code>1</code> | 1.0.0 |
|
|
1797
2039
|
| **`productType`** | <code>string</code> | <a href="#product">Product</a> type. - `"inapp"`: One-time in-app purchase - `"subs"`: Subscription | | 1.0.0 |
|
|
@@ -1805,20 +2047,23 @@ which is useful for determining if users are entitled to features from earlier b
|
|
|
1805
2047
|
|
|
1806
2048
|
#### Product
|
|
1807
2049
|
|
|
1808
|
-
| Prop | Type | Description
|
|
1809
|
-
| --------------------------------- | ----------------------------------------------------------------------- |
|
|
1810
|
-
| **`identifier`** | <code>string</code> | <a href="#product">Product</a> Id.
|
|
1811
|
-
| **`description`** | <code>string</code> | Description of the product.
|
|
1812
|
-
| **`title`** | <code>string</code> | Title of the product.
|
|
1813
|
-
| **`price`** | <code>number</code> | Price of the product in the local currency.
|
|
1814
|
-
| **`priceString`** | <code>string</code> | Formatted price of the item, including its currency sign, such as €3.99.
|
|
1815
|
-
| **`currencyCode`** | <code>string</code> | Currency code for price and original price.
|
|
1816
|
-
| **`currencySymbol`** | <code>string</code> | Currency symbol for price and original price.
|
|
1817
|
-
| **`isFamilyShareable`** | <code>boolean</code> | Boolean indicating if the product is sharable with family
|
|
1818
|
-
| **`subscriptionGroupIdentifier`** | <code>string</code> | Group identifier for the product.
|
|
1819
|
-
| **`
|
|
1820
|
-
| **`
|
|
1821
|
-
| **`
|
|
2050
|
+
| Prop | Type | Description |
|
|
2051
|
+
| --------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2052
|
+
| **`identifier`** | <code>string</code> | <a href="#product">Product</a> Id. Android subscriptions note: - `identifier` is the base plan ID (`offerDetails.getBasePlanId()`). - `planIdentifier` is the subscription product ID (`productDetails.getProductId()`). If you group/filter Android subscription results by `identifier`, you are grouping by base plan. |
|
|
2053
|
+
| **`description`** | <code>string</code> | Description of the product. |
|
|
2054
|
+
| **`title`** | <code>string</code> | Title of the product. |
|
|
2055
|
+
| **`price`** | <code>number</code> | Price of the product in the local currency. |
|
|
2056
|
+
| **`priceString`** | <code>string</code> | Formatted price of the item, including its currency sign, such as €3.99. |
|
|
2057
|
+
| **`currencyCode`** | <code>string</code> | Currency code for price and original price. |
|
|
2058
|
+
| **`currencySymbol`** | <code>string</code> | Currency symbol for price and original price. |
|
|
2059
|
+
| **`isFamilyShareable`** | <code>boolean</code> | Boolean indicating if the product is sharable with family |
|
|
2060
|
+
| **`subscriptionGroupIdentifier`** | <code>string</code> | Group identifier for the product. |
|
|
2061
|
+
| **`planIdentifier`** | <code>string</code> | Android subscriptions only: Google Play product identifier tied to the offer/base plan set. |
|
|
2062
|
+
| **`offerToken`** | <code>string</code> | Android subscriptions only: offer token required when purchasing specific offers. |
|
|
2063
|
+
| **`offerId`** | <code>string \| null</code> | Android subscriptions only: offer identifier (null/undefined for base offers). |
|
|
2064
|
+
| **`subscriptionPeriod`** | <code><a href="#subscriptionperiod">SubscriptionPeriod</a></code> | The <a href="#product">Product</a> subscription group identifier. |
|
|
2065
|
+
| **`introductoryPrice`** | <code><a href="#skproductdiscount">SKProductDiscount</a> \| null</code> | The <a href="#product">Product</a> introductory Price. |
|
|
2066
|
+
| **`discounts`** | <code>SKProductDiscount[]</code> | The <a href="#product">Product</a> discounts list. |
|
|
1822
2067
|
|
|
1823
2068
|
|
|
1824
2069
|
#### SubscriptionPeriod
|
package/android/build.gradle
CHANGED
|
@@ -30,7 +30,7 @@ android {
|
|
|
30
30
|
buildTypes {
|
|
31
31
|
release {
|
|
32
32
|
minifyEnabled false
|
|
33
|
-
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
|
33
|
+
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
lintOptions {
|
|
@@ -50,7 +50,7 @@ repositories {
|
|
|
50
50
|
|
|
51
51
|
dependencies {
|
|
52
52
|
implementation "com.google.guava:guava:33.5.0-android"
|
|
53
|
-
def billing_version = "8.
|
|
53
|
+
def billing_version = "8.3.0"
|
|
54
54
|
implementation "com.android.billingclient:billing:$billing_version"
|
|
55
55
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
56
56
|
implementation project(':capacitor-android')
|