expo-iap 2.3.4 → 2.3.5-rc.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/build/modules/ios.js.map +1 -1
- package/iap.md +20 -38
- package/ios/ExpoIapModule.swift +36 -30
- package/package.json +1 -1
- package/src/modules/ios.ts +5 -5
package/build/modules/ios.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ios.js","sourceRoot":"","sources":["../../src/modules/ios.ts"],"names":[],"mappings":"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","sourcesContent":["import {Platform} from 'react-native';\nimport {emitter, IapEvent} from '..';\nimport {ProductPurchase, PurchaseError} from '../ExpoIap.types';\nimport type {ProductStatusIos} from '../types/ExpoIapIos.types';\nimport ExpoIapModule from '../ExpoIapModule';\n\nexport type TransactionEvent = {\n transaction?: ProductPurchase;\n error?: PurchaseError;\n};\n\n// Listeners\nexport const transactionUpdatedIos = (\n listener: (event: TransactionEvent) => void,\n) => {\n if (Platform.OS !== 'ios') {\n throw new Error('This method is only available on iOS');\n }\n\n return emitter.addListener(IapEvent.TransactionIapUpdated, listener);\n};\n\n// Type guards\nexport function isProductIos<T extends {platform?: string}>(\n item: unknown,\n): item is T & {platform: 'ios'} {\n return (\n item != null &&\n typeof item === 'object' &&\n 'platform' in item &&\n item.platform === 'ios'\n );\n}\n\n// Functions\n/**\n * Sync state with Appstore (iOS only)\n * https://developer.apple.com/documentation/storekit/appstore/3791906-sync\n */\nexport const sync = (): Promise<null> => ExpoIapModule.sync();\n\n/**\n *\n */\nexport const isEligibleForIntroOffer = (groupID: string): Promise<boolean> =>\n ExpoIapModule.isEligibleForIntroOffer(groupID);\n\n/**\n *\n */\n\nexport const subscriptionStatus = (sku: string): Promise<ProductStatusIos[]> =>\n ExpoIapModule.subscriptionStatus(sku);\n\n/**\n *\n */\nexport const currentEntitlement = (sku: string): Promise<ProductPurchase> =>\n ExpoIapModule.currentEntitlement(sku);\n\n/**\n *\n */\nexport const latestTransaction = (sku: string): Promise<ProductPurchase> =>\n ExpoIapModule.latestTransaction(sku);\n\n/**\n *\n */\ntype RefundRequestStatus = 'success' | 'userCancelled';\nexport const beginRefundRequest = (sku: string): Promise<RefundRequestStatus> =>\n ExpoIapModule.beginRefundRequest(sku);\n\n/**\n * Shows the system UI for managing subscriptions.\n * When the user changes subscription renewal status, the system will emit events to\n * purchaseUpdatedListener and transactionUpdatedIos listeners.\n * @returns {Promise<null>}\n */\nexport const showManageSubscriptions = (): Promise<null> =>\n ExpoIapModule.showManageSubscriptions();\n\n/**\n * Get the receipt data from the iOS device.\n * This returns the base64 encoded receipt data which can be sent to your server\n * for verification with Apple's server.\n
|
|
1
|
+
{"version":3,"file":"ios.js","sourceRoot":"","sources":["../../src/modules/ios.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,QAAQ,EAAC,MAAM,cAAc,CAAC;AACtC,OAAO,EAAC,OAAO,EAAE,QAAQ,EAAC,MAAM,IAAI,CAAC;AAGrC,OAAO,aAAa,MAAM,kBAAkB,CAAC;AAO7C,YAAY;AACZ,MAAM,CAAC,MAAM,qBAAqB,GAAG,CACnC,QAA2C,EAC3C,EAAE;IACF,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1D,CAAC;IAED,OAAO,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,qBAAqB,EAAE,QAAQ,CAAC,CAAC;AACvE,CAAC,CAAC;AAEF,cAAc;AACd,MAAM,UAAU,YAAY,CAC1B,IAAa;IAEb,OAAO,CACL,IAAI,IAAI,IAAI;QACZ,OAAO,IAAI,KAAK,QAAQ;QACxB,UAAU,IAAI,IAAI;QAClB,IAAI,CAAC,QAAQ,KAAK,KAAK,CACxB,CAAC;AACJ,CAAC;AAED,YAAY;AACZ;;;GAGG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,GAAkB,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,CAAC;AAE9D;;GAEG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,OAAe,EAAoB,EAAE,CAC3E,aAAa,CAAC,uBAAuB,CAAC,OAAO,CAAC,CAAC;AAEjD;;GAEG;AAEH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,GAAW,EAA+B,EAAE,CAC7E,aAAa,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC;AAExC;;GAEG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,GAAW,EAA4B,EAAE,CAC1E,aAAa,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC;AAExC;;GAEG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,GAAW,EAA4B,EAAE,CACzE,aAAa,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC;AAMvC,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,GAAW,EAAgC,EAAE,CAC9E,aAAa,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC;AAExC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAkB,EAAE,CACzD,aAAa,CAAC,uBAAuB,EAAE,CAAC;AAE1C;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,GAAoB,EAAE;IACjD,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1D,CAAC;IACD,OAAO,aAAa,CAAC,cAAc,EAAE,CAAC;AACxC,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,GAAW,EAAoB,EAAE;IACrE,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1D,CAAC;IACD,OAAO,aAAa,CAAC,qBAAqB,CAAC,GAAG,CAAC,CAAC;AAClD,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,GAAW,EAAmB,EAAE;IAChE,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1D,CAAC;IACD,OAAO,aAAa,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC;AAC9C,CAAC,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,EACrC,GAAW,EAMV,EAAE;IACH,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1D,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC;IAC3D,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC","sourcesContent":["import {Platform} from 'react-native';\nimport {emitter, IapEvent} from '..';\nimport {ProductPurchase, PurchaseError} from '../ExpoIap.types';\nimport type {ProductStatusIos} from '../types/ExpoIapIos.types';\nimport ExpoIapModule from '../ExpoIapModule';\n\nexport type TransactionEvent = {\n transaction?: ProductPurchase;\n error?: PurchaseError;\n};\n\n// Listeners\nexport const transactionUpdatedIos = (\n listener: (event: TransactionEvent) => void,\n) => {\n if (Platform.OS !== 'ios') {\n throw new Error('This method is only available on iOS');\n }\n\n return emitter.addListener(IapEvent.TransactionIapUpdated, listener);\n};\n\n// Type guards\nexport function isProductIos<T extends {platform?: string}>(\n item: unknown,\n): item is T & {platform: 'ios'} {\n return (\n item != null &&\n typeof item === 'object' &&\n 'platform' in item &&\n item.platform === 'ios'\n );\n}\n\n// Functions\n/**\n * Sync state with Appstore (iOS only)\n * https://developer.apple.com/documentation/storekit/appstore/3791906-sync\n */\nexport const sync = (): Promise<null> => ExpoIapModule.sync();\n\n/**\n *\n */\nexport const isEligibleForIntroOffer = (groupID: string): Promise<boolean> =>\n ExpoIapModule.isEligibleForIntroOffer(groupID);\n\n/**\n *\n */\n\nexport const subscriptionStatus = (sku: string): Promise<ProductStatusIos[]> =>\n ExpoIapModule.subscriptionStatus(sku);\n\n/**\n *\n */\nexport const currentEntitlement = (sku: string): Promise<ProductPurchase> =>\n ExpoIapModule.currentEntitlement(sku);\n\n/**\n *\n */\nexport const latestTransaction = (sku: string): Promise<ProductPurchase> =>\n ExpoIapModule.latestTransaction(sku);\n\n/**\n *\n */\ntype RefundRequestStatus = 'success' | 'userCancelled';\nexport const beginRefundRequest = (sku: string): Promise<RefundRequestStatus> =>\n ExpoIapModule.beginRefundRequest(sku);\n\n/**\n * Shows the system UI for managing subscriptions.\n * When the user changes subscription renewal status, the system will emit events to \n * purchaseUpdatedListener and transactionUpdatedIos listeners.\n * @returns {Promise<null>}\n */\nexport const showManageSubscriptions = (): Promise<null> =>\n ExpoIapModule.showManageSubscriptions();\n\n/**\n * Get the receipt data from the iOS device.\n * This returns the base64 encoded receipt data which can be sent to your server\n * for verification with Apple's server.\n * \n * NOTE: For proper security, always verify receipts on your server using\n * Apple's verifyReceipt endpoint, not directly from the app.\n * \n * @returns {Promise<string>} Base64 encoded receipt data\n */\nexport const getReceiptIos = (): Promise<string> => {\n if (Platform.OS !== 'ios') {\n throw new Error('This method is only available on iOS');\n }\n return ExpoIapModule.getReceiptData();\n};\n\n/**\n * Check if a transaction is verified through StoreKit 2.\n * StoreKit 2 performs local verification of transaction JWS signatures.\n * \n * @param {string} sku The product's SKU (on iOS)\n * @returns {Promise<boolean>} True if the transaction is verified\n */\nexport const isTransactionVerified = (sku: string): Promise<boolean> => {\n if (Platform.OS !== 'ios') {\n throw new Error('This method is only available on iOS');\n }\n return ExpoIapModule.isTransactionVerified(sku);\n};\n\n/**\n * Get the JWS representation of a purchase for server-side verification.\n * The JWS (JSON Web Signature) can be verified on your server using Apple's public keys.\n * \n * @param {string} sku The product's SKU (on iOS)\n * @returns {Promise<string>} JWS representation of the transaction\n */\nexport const getTransactionJws = (sku: string): Promise<string> => {\n if (Platform.OS !== 'ios') {\n throw new Error('This method is only available on iOS');\n }\n return ExpoIapModule.getTransactionJws(sku);\n};\n\n/**\n * Validate receipt for iOS using StoreKit 2's built-in verification.\n * Returns receipt data and verification information to help with server-side validation.\n *\n * NOTE: For proper security, Apple recommends verifying receipts on your server using\n * the verifyReceipt endpoint rather than relying solely on client-side verification.\n *\n * @param {string} sku The product's SKU (on iOS)\n * @returns {Promise<{\n * isValid: boolean;\n * receiptData: string;\n * jwsRepresentation: string;\n * latestTransaction?: ProductPurchase;\n * }>}\n */\nexport const validateReceiptIos = async (\n sku: string,\n): Promise<{\n isValid: boolean;\n receiptData: string;\n jwsRepresentation: string;\n latestTransaction?: ProductPurchase;\n}> => {\n if (Platform.OS !== 'ios') {\n throw new Error('This method is only available on iOS');\n }\n\n const result = await ExpoIapModule.validateReceiptIos(sku);\n return result;\n};\n"]}
|
package/iap.md
CHANGED
|
@@ -209,8 +209,8 @@ Transactions map to `Purchase` or `SubscriptionPurchase` with platform-specific
|
|
|
209
209
|
Below is a simple example of fetching products and making a purchase with `expo-iap` in a managed workflow, updated to use the new `requestPurchase` signature:
|
|
210
210
|
|
|
211
211
|
```tsx
|
|
212
|
-
import {useEffect, useState} from 'react';
|
|
213
|
-
import {Button, Text, View} from 'react-native';
|
|
212
|
+
import { useEffect, useState } from 'react';
|
|
213
|
+
import { Button, Text, View } from 'react-native';
|
|
214
214
|
import {
|
|
215
215
|
initConnection,
|
|
216
216
|
endConnection,
|
|
@@ -237,7 +237,7 @@ export default function SimpleIAP() {
|
|
|
237
237
|
|
|
238
238
|
const purchaseListener = purchaseUpdatedListener(async (purchase) => {
|
|
239
239
|
if (purchase) {
|
|
240
|
-
await finishTransaction({purchase, isConsumable: true});
|
|
240
|
+
await finishTransaction({ purchase, isConsumable: true });
|
|
241
241
|
alert('Purchase completed!');
|
|
242
242
|
}
|
|
243
243
|
});
|
|
@@ -252,12 +252,12 @@ export default function SimpleIAP() {
|
|
|
252
252
|
const buyItem = async () => {
|
|
253
253
|
if (!product) return;
|
|
254
254
|
await requestPurchase({
|
|
255
|
-
request: {skus: [product.id]}, // Android expects 'skus'; iOS would use 'sku'
|
|
255
|
+
request: { skus: [product.id] }, // Android expects 'skus'; iOS would use 'sku'
|
|
256
256
|
});
|
|
257
257
|
};
|
|
258
258
|
|
|
259
259
|
return (
|
|
260
|
-
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
|
|
260
|
+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
|
261
261
|
<Text>{isConnected ? 'Connected' : 'Connecting...'}</Text>
|
|
262
262
|
{product ? (
|
|
263
263
|
<>
|
|
@@ -277,7 +277,7 @@ export default function SimpleIAP() {
|
|
|
277
277
|
The `useIAP` hook simplifies managing in-app purchases. Below is an example updated to use the new `requestPurchase` signature:
|
|
278
278
|
|
|
279
279
|
```tsx
|
|
280
|
-
import {useEffect, useState} from 'react';
|
|
280
|
+
import { useEffect, useState } from 'react';
|
|
281
281
|
import {
|
|
282
282
|
SafeAreaView,
|
|
283
283
|
ScrollView,
|
|
@@ -289,20 +289,12 @@ import {
|
|
|
289
289
|
InteractionManager,
|
|
290
290
|
Alert,
|
|
291
291
|
} from 'react-native';
|
|
292
|
-
import {useIAP} from 'expo-iap';
|
|
293
|
-
import type {ProductPurchase, SubscriptionProduct} from 'expo-iap';
|
|
292
|
+
import { useIAP } from 'expo-iap';
|
|
293
|
+
import type { ProductPurchase, SubscriptionProduct } from 'expo-iap';
|
|
294
294
|
|
|
295
295
|
// Define SKUs
|
|
296
|
-
const productSkus = [
|
|
297
|
-
|
|
298
|
-
'cpk.points.5000',
|
|
299
|
-
'cpk.points.10000',
|
|
300
|
-
'cpk.points.30000',
|
|
301
|
-
];
|
|
302
|
-
const subscriptionSkus = [
|
|
303
|
-
'cpk.membership.monthly.bronze',
|
|
304
|
-
'cpk.membership.monthly.silver',
|
|
305
|
-
];
|
|
296
|
+
const productSkus = ['cpk.points.1000', 'cpk.points.5000', 'cpk.points.10000', 'cpk.points.30000'];
|
|
297
|
+
const subscriptionSkus = ['cpk.membership.monthly.bronze', 'cpk.membership.monthly.silver'];
|
|
306
298
|
|
|
307
299
|
// Define operations
|
|
308
300
|
const operations = ['getProducts', 'getSubscriptions'] as const;
|
|
@@ -329,10 +321,7 @@ export default function IAPWithHook() {
|
|
|
329
321
|
|
|
330
322
|
const initializeIAP = async () => {
|
|
331
323
|
try {
|
|
332
|
-
await Promise.all([
|
|
333
|
-
getProducts(productSkus),
|
|
334
|
-
getSubscriptions(subscriptionSkus),
|
|
335
|
-
]);
|
|
324
|
+
await Promise.all([getProducts(productSkus), getSubscriptions(subscriptionSkus)]);
|
|
336
325
|
setIsReady(true);
|
|
337
326
|
} catch (error) {
|
|
338
327
|
console.error('Error initializing IAP:', error);
|
|
@@ -400,10 +389,7 @@ export default function IAPWithHook() {
|
|
|
400
389
|
<View style={styles.buttons}>
|
|
401
390
|
<ScrollView contentContainerStyle={styles.buttonsWrapper} horizontal>
|
|
402
391
|
{operations.map((operation) => (
|
|
403
|
-
<Pressable
|
|
404
|
-
key={operation}
|
|
405
|
-
onPress={() => handleOperation(operation)}
|
|
406
|
-
>
|
|
392
|
+
<Pressable key={operation} onPress={() => handleOperation(operation)}>
|
|
407
393
|
<View style={styles.buttonView}>
|
|
408
394
|
<Text>{operation}</Text>
|
|
409
395
|
</View>
|
|
@@ -415,10 +401,10 @@ export default function IAPWithHook() {
|
|
|
415
401
|
{!isReady ? (
|
|
416
402
|
<Text>Loading...</Text>
|
|
417
403
|
) : (
|
|
418
|
-
<View style={{gap: 12}}>
|
|
419
|
-
<Text style={{fontSize: 20}}>Products</Text>
|
|
404
|
+
<View style={{ gap: 12 }}>
|
|
405
|
+
<Text style={{ fontSize: 20 }}>Products</Text>
|
|
420
406
|
{products.map((item) => (
|
|
421
|
-
<View key={item.id} style={{gap: 12}}>
|
|
407
|
+
<View key={item.id} style={{ gap: 12 }}>
|
|
422
408
|
<Text>
|
|
423
409
|
{item.title} -{' '}
|
|
424
410
|
{item.platform === 'android'
|
|
@@ -429,24 +415,20 @@ export default function IAPWithHook() {
|
|
|
429
415
|
title="Buy"
|
|
430
416
|
onPress={() =>
|
|
431
417
|
requestPurchase({
|
|
432
|
-
request:
|
|
433
|
-
item.platform === 'android'
|
|
434
|
-
? {skus: [item.id]}
|
|
435
|
-
: {sku: item.id},
|
|
418
|
+
request: item.platform === 'android' ? { skus: [item.id] } : { sku: item.id },
|
|
436
419
|
})
|
|
437
420
|
}
|
|
438
421
|
/>
|
|
439
422
|
</View>
|
|
440
423
|
))}
|
|
441
424
|
|
|
442
|
-
<Text style={{fontSize: 20}}>Subscriptions</Text>
|
|
425
|
+
<Text style={{ fontSize: 20 }}>Subscriptions</Text>
|
|
443
426
|
{subscriptions.map((item) => (
|
|
444
|
-
<View key={item.id} style={{gap: 12}}>
|
|
427
|
+
<View key={item.id} style={{ gap: 12 }}>
|
|
445
428
|
<Text>
|
|
446
429
|
{item.title || item.displayName} -{' '}
|
|
447
430
|
{item.platform === 'android' && item.subscriptionOfferDetails
|
|
448
|
-
? item.subscriptionOfferDetails[0]?.pricingPhases
|
|
449
|
-
.pricingPhaseList[0].formattedPrice
|
|
431
|
+
? item.subscriptionOfferDetails[0]?.pricingPhases.pricingPhaseList[0].formattedPrice
|
|
450
432
|
: item.displayPrice}
|
|
451
433
|
</Text>
|
|
452
434
|
<Button
|
|
@@ -463,7 +445,7 @@ export default function IAPWithHook() {
|
|
|
463
445
|
offerToken: offer.offerToken,
|
|
464
446
|
})) || [],
|
|
465
447
|
}
|
|
466
|
-
: {sku: item.id},
|
|
448
|
+
: { sku: item.id },
|
|
467
449
|
type: 'subs',
|
|
468
450
|
})
|
|
469
451
|
}
|
package/ios/ExpoIapModule.swift
CHANGED
|
@@ -71,7 +71,10 @@ func serializeTransaction(_ transaction: Transaction, jwsRepresentationIos: Stri
|
|
|
71
71
|
]
|
|
72
72
|
|
|
73
73
|
if (jwsRepresentationIos != nil) {
|
|
74
|
+
print("DEBUG - serializeTransaction adding jwsRepresentationIos with length: \(jwsRepresentationIos!.count)")
|
|
74
75
|
purchaseMap["jwsRepresentationIos"] = jwsRepresentationIos
|
|
76
|
+
} else {
|
|
77
|
+
print("DEBUG - serializeTransaction jwsRepresentationIos is nil")
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
if #available(iOS 16.0, *) {
|
|
@@ -237,15 +240,20 @@ public class ExpoIapModule: Module {
|
|
|
237
240
|
var purchasedItemsSerialized: [[String: Any?]] = []
|
|
238
241
|
|
|
239
242
|
func addTransaction(transaction: Transaction, jwsRepresentationIos: String? = nil) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
+
// Debug: Log JWS representation
|
|
244
|
+
print("DEBUG - getAvailableItems JWS: \(jwsRepresentationIos != nil ? "exists" : "nil")")
|
|
245
|
+
if let jws = jwsRepresentationIos {
|
|
246
|
+
print("DEBUG - getAvailableItems JWS length: \(jws.count)")
|
|
243
247
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
248
|
+
|
|
249
|
+
let serialized = serializeTransaction(transaction, jwsRepresentationIos: jwsRepresentationIos)
|
|
250
|
+
purchasedItemsSerialized.append(serialized)
|
|
251
|
+
|
|
252
|
+
// Debug: Check if jwsRepresentationIos is included in serialized result
|
|
253
|
+
print("DEBUG - getAvailableItems serialized includes JWS: \(serialized["jwsRepresentationIos"] != nil)")
|
|
254
|
+
|
|
247
255
|
if alsoPublishToEventListener {
|
|
248
|
-
self.sendEvent(IapEvent.
|
|
256
|
+
self.sendEvent(IapEvent.PurchaseUpdated, serialized)
|
|
249
257
|
}
|
|
250
258
|
}
|
|
251
259
|
|
|
@@ -287,7 +295,9 @@ public class ExpoIapModule: Module {
|
|
|
287
295
|
"message": StoreError.failedVerification.localizedDescription,
|
|
288
296
|
"productId": "unknown",
|
|
289
297
|
]
|
|
290
|
-
|
|
298
|
+
if alsoPublishToEventListener {
|
|
299
|
+
self.sendEvent(IapEvent.PurchaseError, err)
|
|
300
|
+
}
|
|
291
301
|
} catch {
|
|
292
302
|
let err = [
|
|
293
303
|
"responseCode": IapErrors.E_UNKNOWN.rawValue,
|
|
@@ -296,10 +306,11 @@ public class ExpoIapModule: Module {
|
|
|
296
306
|
"message": error.localizedDescription,
|
|
297
307
|
"productId": "unknown",
|
|
298
308
|
]
|
|
299
|
-
|
|
309
|
+
if alsoPublishToEventListener {
|
|
310
|
+
self.sendEvent(IapEvent.PurchaseError, err)
|
|
311
|
+
}
|
|
300
312
|
}
|
|
301
313
|
}
|
|
302
|
-
|
|
303
314
|
return purchasedItemsSerialized
|
|
304
315
|
}
|
|
305
316
|
|
|
@@ -362,12 +373,21 @@ public class ExpoIapModule: Module {
|
|
|
362
373
|
switch result {
|
|
363
374
|
case .success(let verification):
|
|
364
375
|
let transaction = try self.checkVerified(verification)
|
|
376
|
+
|
|
377
|
+
// Debug: Log JWS representation
|
|
378
|
+
print("DEBUG - buyProduct JWS: exists")
|
|
379
|
+
print("DEBUG - buyProduct JWS length: \(verification.jwsRepresentation.count)")
|
|
380
|
+
|
|
365
381
|
if andDangerouslyFinishTransactionAutomatically {
|
|
366
382
|
await transaction.finish()
|
|
367
383
|
return nil
|
|
368
384
|
} else {
|
|
369
385
|
self.transactions[String(transaction.id)] = transaction
|
|
370
386
|
let serialized = serializeTransaction(transaction, jwsRepresentationIos: verification.jwsRepresentation)
|
|
387
|
+
|
|
388
|
+
// Debug: Check if jwsRepresentationIos is included in serialized result
|
|
389
|
+
print("DEBUG - buyProduct serialized includes JWS: \(serialized["jwsRepresentationIos"] != nil)")
|
|
390
|
+
|
|
371
391
|
self.sendEvent(IapEvent.PurchaseUpdated, serialized)
|
|
372
392
|
return serialized
|
|
373
393
|
}
|
|
@@ -458,9 +478,7 @@ public class ExpoIapModule: Module {
|
|
|
458
478
|
} else {
|
|
459
479
|
throw NSError(
|
|
460
480
|
domain: "ExpoIapModule", code: 4,
|
|
461
|
-
userInfo: [
|
|
462
|
-
NSLocalizedDescriptionKey: "Can't find entitlement for sku \(sku)"
|
|
463
|
-
])
|
|
481
|
+
userInfo: [NSLocalizedDescriptionKey: "Can't find entitlement for sku \(sku)"])
|
|
464
482
|
}
|
|
465
483
|
} else {
|
|
466
484
|
throw NSError(
|
|
@@ -499,10 +517,7 @@ public class ExpoIapModule: Module {
|
|
|
499
517
|
} else {
|
|
500
518
|
throw NSError(
|
|
501
519
|
domain: "ExpoIapModule", code: 4,
|
|
502
|
-
userInfo: [
|
|
503
|
-
NSLocalizedDescriptionKey:
|
|
504
|
-
"Can't find latest transaction for sku \(sku)"
|
|
505
|
-
])
|
|
520
|
+
userInfo: [NSLocalizedDescriptionKey: "Can't find latest transaction for sku \(sku)"])
|
|
506
521
|
}
|
|
507
522
|
} else {
|
|
508
523
|
throw NSError(
|
|
@@ -562,17 +577,13 @@ public class ExpoIapModule: Module {
|
|
|
562
577
|
"Cannot find window scene or not available on macOS"
|
|
563
578
|
])
|
|
564
579
|
}
|
|
565
|
-
|
|
566
580
|
// Get all subscription products before showing the management UI
|
|
567
581
|
let subscriptionSkus = await self.getAllSubscriptionProductIds()
|
|
568
582
|
self.pollingSkus = Set(subscriptionSkus)
|
|
569
|
-
|
|
570
583
|
// Show the management UI
|
|
571
584
|
try await AppStore.showManageSubscriptions(in: windowScene)
|
|
572
|
-
|
|
573
585
|
// Start polling for status changes
|
|
574
586
|
self.pollForSubscriptionStatusChanges()
|
|
575
|
-
|
|
576
587
|
return true
|
|
577
588
|
#else
|
|
578
589
|
throw NSError(
|
|
@@ -649,7 +660,7 @@ public class ExpoIapModule: Module {
|
|
|
649
660
|
AsyncFunction("getReceiptData") { () -> String? in
|
|
650
661
|
return try self.getReceiptDataInternal()
|
|
651
662
|
}
|
|
652
|
-
|
|
663
|
+
|
|
653
664
|
AsyncFunction("isTransactionVerified") { (sku: String) -> Bool in
|
|
654
665
|
guard let productStore = self.productStore else {
|
|
655
666
|
throw NSError(
|
|
@@ -669,7 +680,7 @@ public class ExpoIapModule: Module {
|
|
|
669
680
|
}
|
|
670
681
|
return false
|
|
671
682
|
}
|
|
672
|
-
|
|
683
|
+
|
|
673
684
|
AsyncFunction("getTransactionJws") { (sku: String) -> String? in
|
|
674
685
|
guard let productStore = self.productStore else {
|
|
675
686
|
throw NSError(
|
|
@@ -686,7 +697,7 @@ public class ExpoIapModule: Module {
|
|
|
686
697
|
userInfo: [NSLocalizedDescriptionKey: "Can't find transaction for sku \(sku)"])
|
|
687
698
|
}
|
|
688
699
|
}
|
|
689
|
-
|
|
700
|
+
|
|
690
701
|
AsyncFunction("validateReceiptIos") { (sku: String) -> [String: Any] in
|
|
691
702
|
guard let productStore = self.productStore else {
|
|
692
703
|
throw NSError(
|
|
@@ -721,7 +732,7 @@ public class ExpoIapModule: Module {
|
|
|
721
732
|
isValid = false
|
|
722
733
|
}
|
|
723
734
|
}
|
|
724
|
-
|
|
735
|
+
|
|
725
736
|
return [
|
|
726
737
|
"isValid": isValid,
|
|
727
738
|
"receiptData": receiptData,
|
|
@@ -808,7 +819,6 @@ public class ExpoIapModule: Module {
|
|
|
808
819
|
|
|
809
820
|
private func pollForSubscriptionStatusChanges() {
|
|
810
821
|
subscriptionPollingTask?.cancel()
|
|
811
|
-
|
|
812
822
|
subscriptionPollingTask = Task {
|
|
813
823
|
try? await Task.sleep(nanoseconds: 1_500_000_000) // 1.5 seconds
|
|
814
824
|
|
|
@@ -828,7 +838,6 @@ public class ExpoIapModule: Module {
|
|
|
828
838
|
|
|
829
839
|
for _ in 1...5 {
|
|
830
840
|
try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds
|
|
831
|
-
|
|
832
841
|
if Task.isCancelled {
|
|
833
842
|
return
|
|
834
843
|
}
|
|
@@ -837,7 +846,6 @@ public class ExpoIapModule: Module {
|
|
|
837
846
|
guard let product = await self.productStore?.getProduct(productID: sku),
|
|
838
847
|
let status = try? await product.subscription?.status.first,
|
|
839
848
|
let result = await product.latestTransaction else { continue }
|
|
840
|
-
|
|
841
849
|
// Try to verify the transaction
|
|
842
850
|
let transaction: Transaction
|
|
843
851
|
do {
|
|
@@ -866,12 +874,10 @@ public class ExpoIapModule: Module {
|
|
|
866
874
|
|
|
867
875
|
self.sendEvent(IapEvent.PurchaseUpdated, purchaseMap)
|
|
868
876
|
self.sendEvent(IapEvent.TransactionIapUpdated, ["transaction": purchaseMap])
|
|
869
|
-
|
|
870
877
|
previousStatuses[sku] = currentWillAutoRenew
|
|
871
878
|
}
|
|
872
879
|
}
|
|
873
880
|
}
|
|
874
|
-
|
|
875
881
|
self.pollingSkus.removeAll()
|
|
876
882
|
}
|
|
877
883
|
}
|
package/package.json
CHANGED
package/src/modules/ios.ts
CHANGED
|
@@ -73,7 +73,7 @@ export const beginRefundRequest = (sku: string): Promise<RefundRequestStatus> =>
|
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
75
|
* Shows the system UI for managing subscriptions.
|
|
76
|
-
* When the user changes subscription renewal status, the system will emit events to
|
|
76
|
+
* When the user changes subscription renewal status, the system will emit events to
|
|
77
77
|
* purchaseUpdatedListener and transactionUpdatedIos listeners.
|
|
78
78
|
* @returns {Promise<null>}
|
|
79
79
|
*/
|
|
@@ -84,10 +84,10 @@ export const showManageSubscriptions = (): Promise<null> =>
|
|
|
84
84
|
* Get the receipt data from the iOS device.
|
|
85
85
|
* This returns the base64 encoded receipt data which can be sent to your server
|
|
86
86
|
* for verification with Apple's server.
|
|
87
|
-
*
|
|
87
|
+
*
|
|
88
88
|
* NOTE: For proper security, always verify receipts on your server using
|
|
89
89
|
* Apple's verifyReceipt endpoint, not directly from the app.
|
|
90
|
-
*
|
|
90
|
+
*
|
|
91
91
|
* @returns {Promise<string>} Base64 encoded receipt data
|
|
92
92
|
*/
|
|
93
93
|
export const getReceiptIos = (): Promise<string> => {
|
|
@@ -100,7 +100,7 @@ export const getReceiptIos = (): Promise<string> => {
|
|
|
100
100
|
/**
|
|
101
101
|
* Check if a transaction is verified through StoreKit 2.
|
|
102
102
|
* StoreKit 2 performs local verification of transaction JWS signatures.
|
|
103
|
-
*
|
|
103
|
+
*
|
|
104
104
|
* @param {string} sku The product's SKU (on iOS)
|
|
105
105
|
* @returns {Promise<boolean>} True if the transaction is verified
|
|
106
106
|
*/
|
|
@@ -114,7 +114,7 @@ export const isTransactionVerified = (sku: string): Promise<boolean> => {
|
|
|
114
114
|
/**
|
|
115
115
|
* Get the JWS representation of a purchase for server-side verification.
|
|
116
116
|
* The JWS (JSON Web Signature) can be verified on your server using Apple's public keys.
|
|
117
|
-
*
|
|
117
|
+
*
|
|
118
118
|
* @param {string} sku The product's SKU (on iOS)
|
|
119
119
|
* @returns {Promise<string>} JWS representation of the transaction
|
|
120
120
|
*/
|