react-native-iap 14.3.7 → 14.3.8
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 +1 -1
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +61 -11
- package/ios/HybridRnIap.swift +47 -12
- package/lib/module/hooks/useIAP.js +31 -21
- package/lib/module/hooks/useIAP.js.map +1 -1
- package/lib/module/index.js +580 -695
- package/lib/module/index.js.map +1 -1
- package/lib/module/types.js +12 -0
- package/lib/module/types.js.map +1 -1
- package/lib/module/utils/purchase.js +22 -0
- package/lib/module/utils/purchase.js.map +1 -0
- package/lib/module/utils.js +43 -0
- package/lib/module/utils.js.map +1 -0
- package/lib/typescript/plugin/src/withIAP.d.ts +1 -0
- package/lib/typescript/plugin/src/withIAP.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useIAP.d.ts +4 -5
- package/lib/typescript/src/hooks/useIAP.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +57 -176
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/RnIap.nitro.d.ts +113 -154
- package/lib/typescript/src/specs/RnIap.nitro.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +99 -76
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/utils/purchase.d.ts +4 -0
- package/lib/typescript/src/utils/purchase.d.ts.map +1 -0
- package/lib/typescript/src/utils.d.ts +8 -0
- package/lib/typescript/src/utils.d.ts.map +1 -0
- package/nitrogen/generated/android/NitroIap+autolinking.cmake +1 -1
- package/nitrogen/generated/android/c++/{JNitroSubscriptionOffer.hpp → JAndroidSubscriptionOfferInput.hpp} +15 -15
- package/nitrogen/generated/android/c++/JFunc_void_NitroProduct.hpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_NitroPurchase.hpp +4 -0
- package/nitrogen/generated/android/c++/JHybridRnIapSpec.cpp +16 -16
- package/nitrogen/generated/android/c++/JHybridRnIapSpec.hpp +1 -1
- package/nitrogen/generated/android/c++/JNitroAvailablePurchasesAndroidOptions.hpp +6 -5
- package/nitrogen/generated/android/c++/JNitroAvailablePurchasesAndroidType.hpp +59 -0
- package/nitrogen/generated/android/c++/JNitroAvailablePurchasesOptions.hpp +2 -1
- package/nitrogen/generated/android/c++/JNitroProduct.hpp +22 -20
- package/nitrogen/generated/android/c++/JNitroPurchase.hpp +12 -8
- package/nitrogen/generated/android/c++/JNitroPurchaseRequest.hpp +2 -2
- package/nitrogen/generated/android/c++/JNitroReceiptValidationAndroidOptions.hpp +10 -10
- package/nitrogen/generated/android/c++/JNitroReceiptValidationResultAndroid.hpp +6 -6
- package/nitrogen/generated/android/c++/JNitroReceiptValidationResultIOS.hpp +4 -0
- package/nitrogen/generated/android/c++/JNitroRequestPurchaseAndroid.hpp +7 -7
- package/nitrogen/generated/android/c++/JRequestPurchaseResult.cpp +39 -0
- package/nitrogen/generated/android/c++/JRequestPurchaseResult.hpp +68 -53
- package/nitrogen/generated/android/c++/JVariant_NitroReceiptValidationResultIOS_NitroReceiptValidationResultAndroid.hpp +4 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/{NitroSubscriptionOffer.kt → AndroidSubscriptionOfferInput.kt} +5 -5
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/HybridRnIapSpec.kt +1 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroAvailablePurchasesAndroidOptions.kt +1 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroAvailablePurchasesAndroidType.kt +21 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroProduct.kt +11 -11
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroPurchase.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroReceiptValidationAndroidOptions.kt +4 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroReceiptValidationResultAndroid.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroRequestPurchaseAndroid.kt +1 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/RequestPurchaseResult.kt +31 -13
- package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Bridge.cpp +4 -4
- package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Bridge.hpp +99 -64
- package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Umbrella.hpp +6 -6
- package/nitrogen/generated/ios/c++/HybridRnIapSpecSwift.hpp +10 -10
- package/nitrogen/generated/ios/swift/{NitroSubscriptionOffer.swift → AndroidSubscriptionOfferInput.swift} +13 -13
- package/nitrogen/generated/ios/swift/Func_void_std__optional_std__variant_PurchaseAndroid__PurchaseIOS__std__vector_std__variant_PurchaseAndroid__PurchaseIOS____.swift +81 -0
- package/nitrogen/generated/ios/swift/HybridRnIapSpec.swift +1 -1
- package/nitrogen/generated/ios/swift/HybridRnIapSpec_cxx.swift +35 -7
- package/nitrogen/generated/ios/swift/NitroAvailablePurchasesAndroidOptions.swift +7 -14
- package/nitrogen/generated/ios/swift/NitroAvailablePurchasesAndroidType.swift +40 -0
- package/nitrogen/generated/ios/swift/NitroProduct.swift +72 -72
- package/nitrogen/generated/ios/swift/NitroPurchase.swift +8 -8
- package/nitrogen/generated/ios/swift/NitroReceiptValidationAndroidOptions.swift +21 -21
- package/nitrogen/generated/ios/swift/NitroReceiptValidationResultAndroid.swift +37 -11
- package/nitrogen/generated/ios/swift/NitroRequestPurchaseAndroid.swift +11 -11
- package/nitrogen/generated/ios/swift/RequestPurchaseResult.swift +8 -137
- package/nitrogen/generated/shared/c++/{NitroSubscriptionOffer.hpp → AndroidSubscriptionOfferInput.hpp} +15 -15
- package/nitrogen/generated/shared/c++/HybridRnIapSpec.hpp +9 -6
- package/nitrogen/generated/shared/c++/NitroAvailablePurchasesAndroidOptions.hpp +8 -7
- package/nitrogen/generated/shared/c++/NitroAvailablePurchasesAndroidType.hpp +76 -0
- package/nitrogen/generated/shared/c++/NitroProduct.hpp +24 -22
- package/nitrogen/generated/shared/c++/NitroPurchase.hpp +15 -10
- package/nitrogen/generated/shared/c++/NitroReceiptValidationAndroidOptions.hpp +10 -10
- package/nitrogen/generated/shared/c++/NitroReceiptValidationResultAndroid.hpp +9 -9
- package/nitrogen/generated/shared/c++/NitroRequestPurchaseAndroid.hpp +8 -8
- package/package.json +2 -2
- package/plugin/build/withIAP.d.ts +1 -0
- package/plugin/build/withIAP.js +8 -2
- package/plugin/src/withIAP.ts +13 -3
- package/src/hooks/useIAP.ts +63 -32
- package/src/index.ts +716 -790
- package/src/specs/RnIap.nitro.ts +131 -163
- package/src/types.ts +131 -85
- package/src/utils/purchase.ts +32 -0
- package/src/utils.ts +68 -0
- package/nitrogen/generated/android/c++/JVariant_PurchaseAndroid_PurchaseIOS.cpp +0 -26
- package/nitrogen/generated/android/c++/JVariant_PurchaseAndroid_PurchaseIOS.hpp +0 -80
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/Variant_PurchaseAndroid_PurchaseIOS.kt +0 -42
- package/nitrogen/generated/ios/swift/Func_void_RequestPurchaseResult.swift +0 -47
- package/nitrogen/generated/ios/swift/Variant_PurchaseAndroid_PurchaseIOS.swift +0 -18
- package/nitrogen/generated/shared/c++/RequestPurchaseResult.hpp +0 -78
package/lib/module/index.js
CHANGED
|
@@ -8,9 +8,11 @@ import { NitroModules } from 'react-native-nitro-modules';
|
|
|
8
8
|
|
|
9
9
|
// Internal modules
|
|
10
10
|
|
|
11
|
-
import { convertNitroProductToProduct, convertNitroPurchaseToPurchase, validateNitroProduct, validateNitroPurchase, convertNitroSubscriptionStatusToSubscriptionStatusIOS } from "./utils/type-bridge.js";
|
|
11
|
+
import { convertNitroProductToProduct, convertNitroPurchaseToPurchase, convertProductToProductSubscription, validateNitroProduct, validateNitroPurchase, convertNitroSubscriptionStatusToSubscriptionStatusIOS } from "./utils/type-bridge.js";
|
|
12
12
|
import { parseErrorStringToJsonObj } from "./utils/error.js";
|
|
13
13
|
import { normalizeErrorCodeFromNative } from "./utils/errorMapping.js";
|
|
14
|
+
import { getSuccessFromPurchaseVariant } from "./utils/purchase.js";
|
|
15
|
+
import { parseAppTransactionPayload } from "./utils.js";
|
|
14
16
|
|
|
15
17
|
// Export all types
|
|
16
18
|
|
|
@@ -23,76 +25,12 @@ const toErrorMessage = error => {
|
|
|
23
25
|
}
|
|
24
26
|
return String(error ?? '');
|
|
25
27
|
};
|
|
26
|
-
const toDiscountOfferRecordIOS = offer => {
|
|
27
|
-
if (!offer) {
|
|
28
|
-
return undefined;
|
|
29
|
-
}
|
|
30
|
-
return {
|
|
31
|
-
identifier: offer.identifier,
|
|
32
|
-
keyIdentifier: offer.keyIdentifier,
|
|
33
|
-
nonce: offer.nonce,
|
|
34
|
-
signature: offer.signature,
|
|
35
|
-
timestamp: String(offer.timestamp)
|
|
36
|
-
};
|
|
37
|
-
};
|
|
38
|
-
function toNitroProductType(type) {
|
|
39
|
-
if (type === 'subs') {
|
|
40
|
-
return 'subs';
|
|
41
|
-
}
|
|
42
|
-
if (type === 'inapp') {
|
|
43
|
-
console.warn(LEGACY_INAPP_WARNING);
|
|
44
|
-
return 'inapp';
|
|
45
|
-
}
|
|
46
|
-
if (type === 'all') {
|
|
47
|
-
return 'inapp';
|
|
48
|
-
}
|
|
49
|
-
return 'inapp';
|
|
50
|
-
}
|
|
51
|
-
function isSubscriptionQuery(type) {
|
|
52
|
-
return type === 'subs';
|
|
53
|
-
}
|
|
54
|
-
function normalizeProductQueryType(type) {
|
|
55
|
-
if (type === 'all' || type === 'subs' || type === 'in-app') {
|
|
56
|
-
return type;
|
|
57
|
-
}
|
|
58
|
-
if (typeof type === 'string') {
|
|
59
|
-
const normalized = type.trim().toLowerCase().replace(/_/g, '-');
|
|
60
|
-
if (normalized === 'all') {
|
|
61
|
-
return 'all';
|
|
62
|
-
}
|
|
63
|
-
if (normalized === 'subs') {
|
|
64
|
-
return 'subs';
|
|
65
|
-
}
|
|
66
|
-
if (normalized === 'inapp') {
|
|
67
|
-
console.warn(LEGACY_INAPP_WARNING);
|
|
68
|
-
return 'in-app';
|
|
69
|
-
}
|
|
70
|
-
if (normalized === 'in-app') {
|
|
71
|
-
return 'in-app';
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return 'in-app';
|
|
75
|
-
}
|
|
76
28
|
// ActiveSubscription and PurchaseError types are already exported via 'export * from ./types'
|
|
77
29
|
|
|
78
30
|
// Export hooks
|
|
79
31
|
export { useIAP } from "./hooks/useIAP.js";
|
|
80
32
|
|
|
81
|
-
// iOS promoted product aliases for API parity
|
|
82
|
-
export const getPromotedProductIOS = async () => requestPromotedProductIOS();
|
|
83
|
-
export const requestPurchaseOnPromotedProductIOS = async () => buyPromotedProductIOS();
|
|
84
|
-
|
|
85
33
|
// Restore completed transactions (cross-platform)
|
|
86
|
-
export const restorePurchases = async (options = {
|
|
87
|
-
alsoPublishToEventListenerIOS: false,
|
|
88
|
-
onlyIncludeActiveItemsIOS: true
|
|
89
|
-
}) => {
|
|
90
|
-
if (Platform.OS === 'ios') {
|
|
91
|
-
await syncIOS();
|
|
92
|
-
}
|
|
93
|
-
return getAvailablePurchases(options);
|
|
94
|
-
};
|
|
95
|
-
|
|
96
34
|
// Development utilities removed - use type bridge functions directly if needed
|
|
97
35
|
|
|
98
36
|
// Create the RnIap HybridObject instance lazily to avoid early JSI crashes
|
|
@@ -115,6 +53,430 @@ const IAP = {
|
|
|
115
53
|
}
|
|
116
54
|
};
|
|
117
55
|
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// EVENT LISTENERS
|
|
58
|
+
// ============================================================================
|
|
59
|
+
|
|
60
|
+
const purchaseUpdatedListenerMap = new WeakMap();
|
|
61
|
+
const purchaseErrorListenerMap = new WeakMap();
|
|
62
|
+
const promotedProductListenerMap = new WeakMap();
|
|
63
|
+
export const purchaseUpdatedListener = listener => {
|
|
64
|
+
const wrappedListener = nitroPurchase => {
|
|
65
|
+
if (validateNitroPurchase(nitroPurchase)) {
|
|
66
|
+
const convertedPurchase = convertNitroPurchaseToPurchase(nitroPurchase);
|
|
67
|
+
listener(convertedPurchase);
|
|
68
|
+
} else {
|
|
69
|
+
console.error('Invalid purchase data received from native:', nitroPurchase);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
purchaseUpdatedListenerMap.set(listener, wrappedListener);
|
|
73
|
+
let attached = false;
|
|
74
|
+
try {
|
|
75
|
+
IAP.instance.addPurchaseUpdatedListener(wrappedListener);
|
|
76
|
+
attached = true;
|
|
77
|
+
} catch (e) {
|
|
78
|
+
const msg = toErrorMessage(e);
|
|
79
|
+
if (msg.includes('Nitro runtime not installed')) {
|
|
80
|
+
console.warn('[purchaseUpdatedListener] Nitro not ready yet; listener inert until initConnection()');
|
|
81
|
+
} else {
|
|
82
|
+
throw e;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
remove: () => {
|
|
87
|
+
const wrapped = purchaseUpdatedListenerMap.get(listener);
|
|
88
|
+
if (wrapped) {
|
|
89
|
+
if (attached) {
|
|
90
|
+
try {
|
|
91
|
+
IAP.instance.removePurchaseUpdatedListener(wrapped);
|
|
92
|
+
} catch {}
|
|
93
|
+
}
|
|
94
|
+
purchaseUpdatedListenerMap.delete(listener);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
export const purchaseErrorListener = listener => {
|
|
100
|
+
const wrapped = error => {
|
|
101
|
+
listener({
|
|
102
|
+
code: normalizeErrorCodeFromNative(error.code),
|
|
103
|
+
message: error.message,
|
|
104
|
+
productId: undefined
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
purchaseErrorListenerMap.set(listener, wrapped);
|
|
108
|
+
let attached = false;
|
|
109
|
+
try {
|
|
110
|
+
IAP.instance.addPurchaseErrorListener(wrapped);
|
|
111
|
+
attached = true;
|
|
112
|
+
} catch (e) {
|
|
113
|
+
const msg = toErrorMessage(e);
|
|
114
|
+
if (msg.includes('Nitro runtime not installed')) {
|
|
115
|
+
console.warn('[purchaseErrorListener] Nitro not ready yet; listener inert until initConnection()');
|
|
116
|
+
} else {
|
|
117
|
+
throw e;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
remove: () => {
|
|
122
|
+
const stored = purchaseErrorListenerMap.get(listener);
|
|
123
|
+
if (stored) {
|
|
124
|
+
if (attached) {
|
|
125
|
+
try {
|
|
126
|
+
IAP.instance.removePurchaseErrorListener(stored);
|
|
127
|
+
} catch {}
|
|
128
|
+
}
|
|
129
|
+
purchaseErrorListenerMap.delete(listener);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
export const promotedProductListenerIOS = listener => {
|
|
135
|
+
if (Platform.OS !== 'ios') {
|
|
136
|
+
console.warn('promotedProductListenerIOS: This listener is only available on iOS');
|
|
137
|
+
return {
|
|
138
|
+
remove: () => {}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const wrappedListener = nitroProduct => {
|
|
142
|
+
if (validateNitroProduct(nitroProduct)) {
|
|
143
|
+
const convertedProduct = convertNitroProductToProduct(nitroProduct);
|
|
144
|
+
listener(convertedProduct);
|
|
145
|
+
} else {
|
|
146
|
+
console.error('Invalid promoted product data received from native:', nitroProduct);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
promotedProductListenerMap.set(listener, wrappedListener);
|
|
150
|
+
let attached = false;
|
|
151
|
+
try {
|
|
152
|
+
IAP.instance.addPromotedProductListenerIOS(wrappedListener);
|
|
153
|
+
attached = true;
|
|
154
|
+
} catch (e) {
|
|
155
|
+
const msg = toErrorMessage(e);
|
|
156
|
+
if (msg.includes('Nitro runtime not installed')) {
|
|
157
|
+
console.warn('[promotedProductListenerIOS] Nitro not ready yet; listener inert until initConnection()');
|
|
158
|
+
} else {
|
|
159
|
+
throw e;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
remove: () => {
|
|
164
|
+
const wrapped = promotedProductListenerMap.get(listener);
|
|
165
|
+
if (wrapped) {
|
|
166
|
+
if (attached) {
|
|
167
|
+
try {
|
|
168
|
+
IAP.instance.removePromotedProductListenerIOS(wrapped);
|
|
169
|
+
} catch {}
|
|
170
|
+
}
|
|
171
|
+
promotedProductListenerMap.delete(listener);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// ------------------------------
|
|
178
|
+
// Query API
|
|
179
|
+
// ------------------------------
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Fetch products from the store
|
|
183
|
+
* @param params - Product request configuration
|
|
184
|
+
* @param params.skus - Array of product SKUs to fetch
|
|
185
|
+
* @param params.type - Optional filter: 'in-app' (default) for products, 'subs' for subscriptions, or 'all' for both.
|
|
186
|
+
* @returns Promise<Product[]> - Array of products from the store
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```typescript
|
|
190
|
+
* // Regular products
|
|
191
|
+
* const products = await fetchProducts({ skus: ['product1', 'product2'] });
|
|
192
|
+
*
|
|
193
|
+
* // Subscriptions
|
|
194
|
+
* const subscriptions = await fetchProducts({ skus: ['sub1', 'sub2'], type: 'subs' });
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
export const fetchProducts = async request => {
|
|
198
|
+
const {
|
|
199
|
+
skus,
|
|
200
|
+
type
|
|
201
|
+
} = request;
|
|
202
|
+
try {
|
|
203
|
+
if (!skus?.length) {
|
|
204
|
+
throw new Error('No SKUs provided');
|
|
205
|
+
}
|
|
206
|
+
const normalizedType = normalizeProductQueryType(type);
|
|
207
|
+
const fetchAndConvert = async nitroType => {
|
|
208
|
+
const nitroProducts = await IAP.instance.fetchProducts(skus, nitroType);
|
|
209
|
+
const validProducts = nitroProducts.filter(validateNitroProduct);
|
|
210
|
+
if (validProducts.length !== nitroProducts.length) {
|
|
211
|
+
console.warn(`[fetchProducts] Some products failed validation: ${nitroProducts.length - validProducts.length} invalid`);
|
|
212
|
+
}
|
|
213
|
+
return validProducts.map(convertNitroProductToProduct);
|
|
214
|
+
};
|
|
215
|
+
if (normalizedType === 'all') {
|
|
216
|
+
const converted = await fetchAndConvert('all');
|
|
217
|
+
const productItems = converted.filter(item => item.type === 'in-app');
|
|
218
|
+
const subscriptionItems = converted.filter(item => item.type === 'subs').map(convertProductToProductSubscription);
|
|
219
|
+
return [...productItems, ...subscriptionItems];
|
|
220
|
+
}
|
|
221
|
+
const convertedProducts = await fetchAndConvert(toNitroProductType(normalizedType));
|
|
222
|
+
if (normalizedType === 'subs') {
|
|
223
|
+
return convertedProducts.map(convertProductToProductSubscription);
|
|
224
|
+
}
|
|
225
|
+
return convertedProducts;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.error('[fetchProducts] Failed:', error);
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get available purchases (purchased items not yet consumed/finished)
|
|
234
|
+
* @param params - Options for getting available purchases
|
|
235
|
+
* @param params.alsoPublishToEventListener - Whether to also publish to event listener
|
|
236
|
+
* @param params.onlyIncludeActiveItems - Whether to only include active items
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* ```typescript
|
|
240
|
+
* const purchases = await getAvailablePurchases({
|
|
241
|
+
* onlyIncludeActiveItemsIOS: true
|
|
242
|
+
* });
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
245
|
+
export const getAvailablePurchases = async options => {
|
|
246
|
+
const alsoPublishToEventListenerIOS = Boolean(options?.alsoPublishToEventListenerIOS ?? false);
|
|
247
|
+
const onlyIncludeActiveItemsIOS = Boolean(options?.onlyIncludeActiveItemsIOS ?? true);
|
|
248
|
+
try {
|
|
249
|
+
if (Platform.OS === 'ios') {
|
|
250
|
+
const nitroOptions = {
|
|
251
|
+
ios: {
|
|
252
|
+
alsoPublishToEventListenerIOS,
|
|
253
|
+
onlyIncludeActiveItemsIOS,
|
|
254
|
+
alsoPublishToEventListener: alsoPublishToEventListenerIOS,
|
|
255
|
+
onlyIncludeActiveItems: onlyIncludeActiveItemsIOS
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
const nitroPurchases = await IAP.instance.getAvailablePurchases(nitroOptions);
|
|
259
|
+
const validPurchases = nitroPurchases.filter(validateNitroPurchase);
|
|
260
|
+
if (validPurchases.length !== nitroPurchases.length) {
|
|
261
|
+
console.warn(`[getAvailablePurchases] Some purchases failed validation: ${nitroPurchases.length - validPurchases.length} invalid`);
|
|
262
|
+
}
|
|
263
|
+
return validPurchases.map(convertNitroPurchaseToPurchase);
|
|
264
|
+
} else if (Platform.OS === 'android') {
|
|
265
|
+
// For Android, we need to call twice for inapp and subs
|
|
266
|
+
const inappNitroPurchases = await IAP.instance.getAvailablePurchases({
|
|
267
|
+
android: {
|
|
268
|
+
type: 'inapp'
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
const subsNitroPurchases = await IAP.instance.getAvailablePurchases({
|
|
272
|
+
android: {
|
|
273
|
+
type: 'subs'
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Validate and convert both sets of purchases
|
|
278
|
+
const allNitroPurchases = [...inappNitroPurchases, ...subsNitroPurchases];
|
|
279
|
+
const validPurchases = allNitroPurchases.filter(validateNitroPurchase);
|
|
280
|
+
if (validPurchases.length !== allNitroPurchases.length) {
|
|
281
|
+
console.warn(`[getAvailablePurchases] Some Android purchases failed validation: ${allNitroPurchases.length - validPurchases.length} invalid`);
|
|
282
|
+
}
|
|
283
|
+
return validPurchases.map(convertNitroPurchaseToPurchase);
|
|
284
|
+
} else {
|
|
285
|
+
throw new Error('Unsupported platform');
|
|
286
|
+
}
|
|
287
|
+
} catch (error) {
|
|
288
|
+
console.error('Failed to get available purchases:', error);
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Request the promoted product from the App Store (iOS only)
|
|
295
|
+
* @returns Promise<Product | null> - The promoted product or null if none available
|
|
296
|
+
* @platform iOS
|
|
297
|
+
*/
|
|
298
|
+
export const getPromotedProductIOS = async () => {
|
|
299
|
+
if (Platform.OS !== 'ios') {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
const nitroProduct = await IAP.instance.requestPromotedProductIOS();
|
|
304
|
+
if (!nitroProduct) {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
const converted = convertNitroProductToProduct(nitroProduct);
|
|
308
|
+
return converted.platform === 'ios' ? converted : null;
|
|
309
|
+
} catch (error) {
|
|
310
|
+
console.error('[getPromotedProductIOS] Failed:', error);
|
|
311
|
+
const errorJson = parseErrorStringToJsonObj(error);
|
|
312
|
+
throw new Error(errorJson.message);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
export const requestPromotedProductIOS = getPromotedProductIOS;
|
|
316
|
+
export const getStorefrontIOS = async () => {
|
|
317
|
+
if (Platform.OS !== 'ios') {
|
|
318
|
+
throw new Error('getStorefrontIOS is only available on iOS');
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
const storefront = await IAP.instance.getStorefrontIOS();
|
|
322
|
+
return storefront;
|
|
323
|
+
} catch (error) {
|
|
324
|
+
console.error('Failed to get storefront:', error);
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
export const getAppTransactionIOS = async () => {
|
|
329
|
+
if (Platform.OS !== 'ios') {
|
|
330
|
+
throw new Error('getAppTransactionIOS is only available on iOS');
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
const appTransaction = await IAP.instance.getAppTransactionIOS();
|
|
334
|
+
if (appTransaction == null) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
if (typeof appTransaction === 'string') {
|
|
338
|
+
const parsed = parseAppTransactionPayload(appTransaction);
|
|
339
|
+
if (parsed) {
|
|
340
|
+
return parsed;
|
|
341
|
+
}
|
|
342
|
+
throw new Error('Unable to parse app transaction payload');
|
|
343
|
+
}
|
|
344
|
+
if (typeof appTransaction === 'object' && appTransaction !== null) {
|
|
345
|
+
return appTransaction;
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
} catch (error) {
|
|
349
|
+
console.error('Failed to get app transaction:', error);
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
export const subscriptionStatusIOS = async sku => {
|
|
354
|
+
if (Platform.OS !== 'ios') {
|
|
355
|
+
throw new Error('subscriptionStatusIOS is only available on iOS');
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
const statuses = await IAP.instance.subscriptionStatusIOS(sku);
|
|
359
|
+
if (!Array.isArray(statuses)) return [];
|
|
360
|
+
return statuses.filter(status => status != null).map(convertNitroSubscriptionStatusToSubscriptionStatusIOS);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
console.error('[subscriptionStatusIOS] Failed:', error);
|
|
363
|
+
const errorJson = parseErrorStringToJsonObj(error);
|
|
364
|
+
throw new Error(errorJson.message);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
export const currentEntitlementIOS = async sku => {
|
|
368
|
+
if (Platform.OS !== 'ios') {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
const nitroPurchase = await IAP.instance.currentEntitlementIOS(sku);
|
|
373
|
+
if (nitroPurchase) {
|
|
374
|
+
const converted = convertNitroPurchaseToPurchase(nitroPurchase);
|
|
375
|
+
return converted.platform === 'ios' ? converted : null;
|
|
376
|
+
}
|
|
377
|
+
return null;
|
|
378
|
+
} catch (error) {
|
|
379
|
+
console.error('[currentEntitlementIOS] Failed:', error);
|
|
380
|
+
const errorJson = parseErrorStringToJsonObj(error);
|
|
381
|
+
throw new Error(errorJson.message);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
export const latestTransactionIOS = async sku => {
|
|
385
|
+
if (Platform.OS !== 'ios') {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
const nitroPurchase = await IAP.instance.latestTransactionIOS(sku);
|
|
390
|
+
if (nitroPurchase) {
|
|
391
|
+
const converted = convertNitroPurchaseToPurchase(nitroPurchase);
|
|
392
|
+
return converted.platform === 'ios' ? converted : null;
|
|
393
|
+
}
|
|
394
|
+
return null;
|
|
395
|
+
} catch (error) {
|
|
396
|
+
console.error('[latestTransactionIOS] Failed:', error);
|
|
397
|
+
const errorJson = parseErrorStringToJsonObj(error);
|
|
398
|
+
throw new Error(errorJson.message);
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
export const getPendingTransactionsIOS = async () => {
|
|
402
|
+
if (Platform.OS !== 'ios') {
|
|
403
|
+
return [];
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
const nitroPurchases = await IAP.instance.getPendingTransactionsIOS();
|
|
407
|
+
return nitroPurchases.map(convertNitroPurchaseToPurchase).filter(purchase => purchase.platform === 'ios');
|
|
408
|
+
} catch (error) {
|
|
409
|
+
console.error('[getPendingTransactionsIOS] Failed:', error);
|
|
410
|
+
const errorJson = parseErrorStringToJsonObj(error);
|
|
411
|
+
throw new Error(errorJson.message);
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
export const showManageSubscriptionsIOS = async () => {
|
|
415
|
+
if (Platform.OS !== 'ios') {
|
|
416
|
+
return [];
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
const nitroPurchases = await IAP.instance.showManageSubscriptionsIOS();
|
|
420
|
+
return nitroPurchases.map(convertNitroPurchaseToPurchase).filter(purchase => purchase.platform === 'ios');
|
|
421
|
+
} catch (error) {
|
|
422
|
+
console.error('[showManageSubscriptionsIOS] Failed:', error);
|
|
423
|
+
const errorJson = parseErrorStringToJsonObj(error);
|
|
424
|
+
throw new Error(errorJson.message);
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
export const isEligibleForIntroOfferIOS = async groupID => {
|
|
428
|
+
if (Platform.OS !== 'ios') {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
return await IAP.instance.isEligibleForIntroOfferIOS(groupID);
|
|
433
|
+
} catch (error) {
|
|
434
|
+
console.error('[isEligibleForIntroOfferIOS] Failed:', error);
|
|
435
|
+
const errorJson = parseErrorStringToJsonObj(error);
|
|
436
|
+
throw new Error(errorJson.message);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
export const getReceiptDataIOS = async () => {
|
|
440
|
+
if (Platform.OS !== 'ios') {
|
|
441
|
+
throw new Error('getReceiptDataIOS is only available on iOS');
|
|
442
|
+
}
|
|
443
|
+
try {
|
|
444
|
+
return await IAP.instance.getReceiptDataIOS();
|
|
445
|
+
} catch (error) {
|
|
446
|
+
console.error('[getReceiptDataIOS] Failed:', error);
|
|
447
|
+
const errorJson = parseErrorStringToJsonObj(error);
|
|
448
|
+
throw new Error(errorJson.message);
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
export const isTransactionVerifiedIOS = async sku => {
|
|
452
|
+
if (Platform.OS !== 'ios') {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
try {
|
|
456
|
+
return await IAP.instance.isTransactionVerifiedIOS(sku);
|
|
457
|
+
} catch (error) {
|
|
458
|
+
console.error('[isTransactionVerifiedIOS] Failed:', error);
|
|
459
|
+
const errorJson = parseErrorStringToJsonObj(error);
|
|
460
|
+
throw new Error(errorJson.message);
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
export const getTransactionJwsIOS = async sku => {
|
|
464
|
+
if (Platform.OS !== 'ios') {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
try {
|
|
468
|
+
return await IAP.instance.getTransactionJwsIOS(sku);
|
|
469
|
+
} catch (error) {
|
|
470
|
+
console.error('[getTransactionJwsIOS] Failed:', error);
|
|
471
|
+
const errorJson = parseErrorStringToJsonObj(error);
|
|
472
|
+
throw new Error(errorJson.message);
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
// ------------------------------
|
|
477
|
+
// Mutation API
|
|
478
|
+
// ------------------------------
|
|
479
|
+
|
|
118
480
|
/**
|
|
119
481
|
* Initialize connection to the store
|
|
120
482
|
*/
|
|
@@ -132,7 +494,6 @@ export const initConnection = async () => {
|
|
|
132
494
|
*/
|
|
133
495
|
export const endConnection = async () => {
|
|
134
496
|
try {
|
|
135
|
-
// If never initialized, treat as ended
|
|
136
497
|
if (!iapRef) return true;
|
|
137
498
|
return await IAP.instance.endConnection();
|
|
138
499
|
} catch (error) {
|
|
@@ -140,111 +501,45 @@ export const endConnection = async () => {
|
|
|
140
501
|
throw error;
|
|
141
502
|
}
|
|
142
503
|
};
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Fetch products from the store
|
|
146
|
-
* @param params - Product request configuration
|
|
147
|
-
* @param params.skus - Array of product SKUs to fetch
|
|
148
|
-
* @param params.type - Optional filter: 'in-app' (default) for products, 'subs' for subscriptions, or 'all' for both.
|
|
149
|
-
* @returns Promise<Product[]> - Array of products from the store
|
|
150
|
-
*
|
|
151
|
-
* @example
|
|
152
|
-
* ```typescript
|
|
153
|
-
* // Regular products
|
|
154
|
-
* const products = await fetchProducts({ skus: ['product1', 'product2'] });
|
|
155
|
-
*
|
|
156
|
-
* // Subscriptions
|
|
157
|
-
* const subscriptions = await fetchProducts({ skus: ['sub1', 'sub2'], type: 'subs' });
|
|
158
|
-
* ```
|
|
159
|
-
*/
|
|
160
|
-
export const fetchProducts = async ({
|
|
161
|
-
skus,
|
|
162
|
-
type = 'in-app'
|
|
163
|
-
}) => {
|
|
504
|
+
export const restorePurchases = async () => {
|
|
164
505
|
try {
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
const normalizedType = normalizeProductQueryType(type);
|
|
169
|
-
if (normalizedType === 'all') {
|
|
170
|
-
const [inappNitro, subsNitro] = await Promise.all([IAP.instance.fetchProducts(skus, 'inapp'), IAP.instance.fetchProducts(skus, 'subs')]);
|
|
171
|
-
const allNitro = [...inappNitro, ...subsNitro];
|
|
172
|
-
const validAll = allNitro.filter(validateNitroProduct);
|
|
173
|
-
if (validAll.length !== allNitro.length) {
|
|
174
|
-
console.warn(`[fetchProducts] Some products failed validation: ${allNitro.length - validAll.length} invalid`);
|
|
175
|
-
}
|
|
176
|
-
return validAll.map(convertNitroProductToProduct);
|
|
177
|
-
}
|
|
178
|
-
const nitroProducts = await IAP.instance.fetchProducts(skus, toNitroProductType(normalizedType));
|
|
179
|
-
|
|
180
|
-
// Validate and convert NitroProducts to TypeScript Products
|
|
181
|
-
const validProducts = nitroProducts.filter(validateNitroProduct);
|
|
182
|
-
if (validProducts.length !== nitroProducts.length) {
|
|
183
|
-
console.warn(`[fetchProducts] Some products failed validation: ${nitroProducts.length - validProducts.length} invalid`);
|
|
506
|
+
if (Platform.OS === 'ios') {
|
|
507
|
+
await syncIOS();
|
|
184
508
|
}
|
|
185
|
-
|
|
186
|
-
|
|
509
|
+
await getAvailablePurchases({
|
|
510
|
+
alsoPublishToEventListenerIOS: false,
|
|
511
|
+
onlyIncludeActiveItemsIOS: true
|
|
512
|
+
});
|
|
187
513
|
} catch (error) {
|
|
188
|
-
console.error('
|
|
514
|
+
console.error('Failed to restore purchases:', error);
|
|
189
515
|
throw error;
|
|
190
516
|
}
|
|
191
517
|
};
|
|
192
518
|
|
|
193
|
-
/**
|
|
194
|
-
* Request a purchase for products or subscriptions
|
|
195
|
-
* @param params - Purchase request configuration
|
|
196
|
-
* @param params.request - Platform-specific purchase parameters
|
|
197
|
-
* @param params.type - Type of purchase: 'in-app' for products (default) or 'subs' for subscriptions
|
|
198
|
-
*
|
|
199
|
-
* @example
|
|
200
|
-
* ```typescript
|
|
201
|
-
* // Product purchase
|
|
202
|
-
* await requestPurchase({
|
|
203
|
-
* request: {
|
|
204
|
-
* ios: { sku: productId },
|
|
205
|
-
* android: { skus: [productId] }
|
|
206
|
-
* },
|
|
207
|
-
* type: 'in-app'
|
|
208
|
-
* });
|
|
209
|
-
*
|
|
210
|
-
* // Subscription purchase
|
|
211
|
-
* await requestPurchase({
|
|
212
|
-
* request: {
|
|
213
|
-
* ios: { sku: subscriptionId },
|
|
214
|
-
* android: {
|
|
215
|
-
* skus: [subscriptionId],
|
|
216
|
-
* subscriptionOffers: [{ sku: subscriptionId, offerToken: 'token' }]
|
|
217
|
-
* }
|
|
218
|
-
* },
|
|
219
|
-
* type: 'subs'
|
|
220
|
-
* });
|
|
221
|
-
* ```
|
|
222
|
-
*/
|
|
223
519
|
/**
|
|
224
520
|
* Request a purchase for products or subscriptions
|
|
225
521
|
* ⚠️ Important: This is an event-based operation, not promise-based.
|
|
226
522
|
* Listen for events through purchaseUpdatedListener or purchaseErrorListener.
|
|
227
|
-
* @param params - Purchase request configuration
|
|
228
|
-
* @param params.request - Platform-specific request parameters
|
|
229
|
-
* @param params.type - Type of purchase (defaults to in-app)
|
|
230
523
|
*/
|
|
231
|
-
export const requestPurchase = async
|
|
524
|
+
export const requestPurchase = async request => {
|
|
232
525
|
try {
|
|
233
|
-
const
|
|
526
|
+
const {
|
|
527
|
+
request: platformRequest,
|
|
528
|
+
type
|
|
529
|
+
} = request;
|
|
530
|
+
const normalizedType = normalizeProductQueryType(type ?? 'in-app');
|
|
234
531
|
const isSubs = isSubscriptionQuery(normalizedType);
|
|
235
|
-
const
|
|
236
|
-
if (!
|
|
532
|
+
const perPlatformRequest = platformRequest;
|
|
533
|
+
if (!perPlatformRequest) {
|
|
237
534
|
throw new Error('Missing purchase request configuration');
|
|
238
535
|
}
|
|
239
|
-
|
|
240
|
-
// Validate platform-specific requests
|
|
241
536
|
if (Platform.OS === 'ios') {
|
|
242
|
-
const iosRequest =
|
|
537
|
+
const iosRequest = perPlatformRequest.ios;
|
|
243
538
|
if (!iosRequest?.sku) {
|
|
244
539
|
throw new Error('Invalid request for iOS. The `sku` property is required.');
|
|
245
540
|
}
|
|
246
541
|
} else if (Platform.OS === 'android') {
|
|
247
|
-
const androidRequest =
|
|
542
|
+
const androidRequest = perPlatformRequest.android;
|
|
248
543
|
if (!androidRequest?.skus?.length) {
|
|
249
544
|
throw new Error('Invalid request for Android. The `skus` property is required and must be a non-empty array.');
|
|
250
545
|
}
|
|
@@ -252,8 +547,8 @@ export const requestPurchase = async params => {
|
|
|
252
547
|
throw new Error('Unsupported platform');
|
|
253
548
|
}
|
|
254
549
|
const unifiedRequest = {};
|
|
255
|
-
if (Platform.OS === 'ios' &&
|
|
256
|
-
const iosRequest = isSubs ?
|
|
550
|
+
if (Platform.OS === 'ios' && perPlatformRequest.ios) {
|
|
551
|
+
const iosRequest = isSubs ? perPlatformRequest.ios : perPlatformRequest.ios;
|
|
257
552
|
const iosPayload = {
|
|
258
553
|
sku: iosRequest.sku
|
|
259
554
|
};
|
|
@@ -274,8 +569,8 @@ export const requestPurchase = async params => {
|
|
|
274
569
|
}
|
|
275
570
|
unifiedRequest.ios = iosPayload;
|
|
276
571
|
}
|
|
277
|
-
if (Platform.OS === 'android' &&
|
|
278
|
-
const androidRequest = isSubs ?
|
|
572
|
+
if (Platform.OS === 'android' && perPlatformRequest.android) {
|
|
573
|
+
const androidRequest = isSubs ? perPlatformRequest.android : perPlatformRequest.android;
|
|
279
574
|
const androidPayload = {
|
|
280
575
|
skus: androidRequest.skus
|
|
281
576
|
};
|
|
@@ -310,75 +605,12 @@ export const requestPurchase = async params => {
|
|
|
310
605
|
}
|
|
311
606
|
};
|
|
312
607
|
|
|
313
|
-
/**
|
|
314
|
-
* Get available purchases (purchased items not yet consumed/finished)
|
|
315
|
-
* @param params - Options for getting available purchases
|
|
316
|
-
* @param params.alsoPublishToEventListener - Whether to also publish to event listener
|
|
317
|
-
* @param params.onlyIncludeActiveItems - Whether to only include active items
|
|
318
|
-
*
|
|
319
|
-
* @example
|
|
320
|
-
* ```typescript
|
|
321
|
-
* const purchases = await getAvailablePurchases({
|
|
322
|
-
* onlyIncludeActiveItemsIOS: true
|
|
323
|
-
* });
|
|
324
|
-
* ```
|
|
325
|
-
*/
|
|
326
|
-
export const getAvailablePurchases = async ({
|
|
327
|
-
alsoPublishToEventListenerIOS = false,
|
|
328
|
-
onlyIncludeActiveItemsIOS = true
|
|
329
|
-
} = {}) => {
|
|
330
|
-
try {
|
|
331
|
-
if (Platform.OS === 'ios') {
|
|
332
|
-
const iosAlsoPublish = Boolean(alsoPublishToEventListenerIOS);
|
|
333
|
-
const iosOnlyActive = Boolean(onlyIncludeActiveItemsIOS);
|
|
334
|
-
const options = {
|
|
335
|
-
ios: {
|
|
336
|
-
alsoPublishToEventListenerIOS: iosAlsoPublish,
|
|
337
|
-
onlyIncludeActiveItemsIOS: iosOnlyActive,
|
|
338
|
-
alsoPublishToEventListener: iosAlsoPublish,
|
|
339
|
-
onlyIncludeActiveItems: iosOnlyActive
|
|
340
|
-
}
|
|
341
|
-
};
|
|
342
|
-
const nitroPurchases = await IAP.instance.getAvailablePurchases(options);
|
|
343
|
-
const validPurchases = nitroPurchases.filter(validateNitroPurchase);
|
|
344
|
-
if (validPurchases.length !== nitroPurchases.length) {
|
|
345
|
-
console.warn(`[getAvailablePurchases] Some purchases failed validation: ${nitroPurchases.length - validPurchases.length} invalid`);
|
|
346
|
-
}
|
|
347
|
-
return validPurchases.map(convertNitroPurchaseToPurchase);
|
|
348
|
-
} else if (Platform.OS === 'android') {
|
|
349
|
-
// For Android, we need to call twice for inapp and subs
|
|
350
|
-
const inappNitroPurchases = await IAP.instance.getAvailablePurchases({
|
|
351
|
-
android: {
|
|
352
|
-
type: 'inapp'
|
|
353
|
-
}
|
|
354
|
-
});
|
|
355
|
-
const subsNitroPurchases = await IAP.instance.getAvailablePurchases({
|
|
356
|
-
android: {
|
|
357
|
-
type: 'subs'
|
|
358
|
-
}
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
// Validate and convert both sets of purchases
|
|
362
|
-
const allNitroPurchases = [...inappNitroPurchases, ...subsNitroPurchases];
|
|
363
|
-
const validPurchases = allNitroPurchases.filter(validateNitroPurchase);
|
|
364
|
-
if (validPurchases.length !== allNitroPurchases.length) {
|
|
365
|
-
console.warn(`[getAvailablePurchases] Some Android purchases failed validation: ${allNitroPurchases.length - validPurchases.length} invalid`);
|
|
366
|
-
}
|
|
367
|
-
return validPurchases.map(convertNitroPurchaseToPurchase);
|
|
368
|
-
} else {
|
|
369
|
-
throw new Error('Unsupported platform');
|
|
370
|
-
}
|
|
371
|
-
} catch (error) {
|
|
372
|
-
console.error('Failed to get available purchases:', error);
|
|
373
|
-
throw error;
|
|
374
|
-
}
|
|
375
|
-
};
|
|
376
|
-
|
|
377
608
|
/**
|
|
378
609
|
* Finish a transaction (consume or acknowledge)
|
|
379
610
|
* @param params - Transaction finish parameters
|
|
380
611
|
* @param params.purchase - The purchase to finish
|
|
381
612
|
* @param params.isConsumable - Whether this is a consumable product (Android only)
|
|
613
|
+
* @returns Promise<void> - Resolves when the transaction is successfully finished
|
|
382
614
|
*
|
|
383
615
|
* @example
|
|
384
616
|
* ```typescript
|
|
@@ -388,10 +620,11 @@ export const getAvailablePurchases = async ({
|
|
|
388
620
|
* });
|
|
389
621
|
* ```
|
|
390
622
|
*/
|
|
391
|
-
export const finishTransaction = async
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
623
|
+
export const finishTransaction = async args => {
|
|
624
|
+
const {
|
|
625
|
+
purchase,
|
|
626
|
+
isConsumable
|
|
627
|
+
} = args;
|
|
395
628
|
try {
|
|
396
629
|
let params;
|
|
397
630
|
if (Platform.OS === 'ios') {
|
|
@@ -404,28 +637,25 @@ export const finishTransaction = async ({
|
|
|
404
637
|
}
|
|
405
638
|
};
|
|
406
639
|
} else if (Platform.OS === 'android') {
|
|
407
|
-
const
|
|
408
|
-
const token = androidPurchase.purchaseToken;
|
|
640
|
+
const token = purchase.purchaseToken ?? undefined;
|
|
409
641
|
if (!token) {
|
|
410
642
|
throw new Error('purchaseToken required to finish Android transaction');
|
|
411
643
|
}
|
|
412
644
|
params = {
|
|
413
645
|
android: {
|
|
414
646
|
purchaseToken: token,
|
|
415
|
-
isConsumable
|
|
647
|
+
isConsumable: isConsumable ?? false
|
|
416
648
|
}
|
|
417
649
|
};
|
|
418
650
|
} else {
|
|
419
651
|
throw new Error('Unsupported platform');
|
|
420
652
|
}
|
|
421
653
|
const result = await IAP.instance.finishTransaction(params);
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
return result;
|
|
654
|
+
const success = getSuccessFromPurchaseVariant(result, 'finishTransaction');
|
|
655
|
+
if (!success) {
|
|
656
|
+
throw new Error('Failed to finish transaction');
|
|
426
657
|
}
|
|
427
|
-
|
|
428
|
-
return result;
|
|
658
|
+
return;
|
|
429
659
|
} catch (error) {
|
|
430
660
|
// If iOS transaction has already been auto-finished natively, treat as success
|
|
431
661
|
if (Platform.OS === 'ios') {
|
|
@@ -434,7 +664,7 @@ export const finishTransaction = async ({
|
|
|
434
664
|
const code = (err?.code || '').toString();
|
|
435
665
|
if (msg.includes('Transaction not found') || code === 'E_ITEM_UNAVAILABLE') {
|
|
436
666
|
// Consider already finished
|
|
437
|
-
return
|
|
667
|
+
return;
|
|
438
668
|
}
|
|
439
669
|
}
|
|
440
670
|
console.error('Failed to finish transaction:', error);
|
|
@@ -445,6 +675,7 @@ export const finishTransaction = async ({
|
|
|
445
675
|
/**
|
|
446
676
|
* Acknowledge a purchase (Android only)
|
|
447
677
|
* @param purchaseToken - The purchase token to acknowledge
|
|
678
|
+
* @returns Promise<boolean> - Indicates whether the acknowledgement succeeded
|
|
448
679
|
*
|
|
449
680
|
* @example
|
|
450
681
|
* ```typescript
|
|
@@ -462,18 +693,7 @@ export const acknowledgePurchaseAndroid = async purchaseToken => {
|
|
|
462
693
|
isConsumable: false
|
|
463
694
|
}
|
|
464
695
|
});
|
|
465
|
-
|
|
466
|
-
// Result is a variant, extract PurchaseResult
|
|
467
|
-
if (typeof result === 'boolean') {
|
|
468
|
-
// This shouldn't happen for Android, but handle it
|
|
469
|
-
return {
|
|
470
|
-
responseCode: 0,
|
|
471
|
-
code: '0',
|
|
472
|
-
message: 'Success',
|
|
473
|
-
purchaseToken
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
return result;
|
|
696
|
+
return getSuccessFromPurchaseVariant(result, 'acknowledgePurchaseAndroid');
|
|
477
697
|
} catch (error) {
|
|
478
698
|
console.error('Failed to acknowledge purchase Android:', error);
|
|
479
699
|
throw error;
|
|
@@ -483,238 +703,29 @@ export const acknowledgePurchaseAndroid = async purchaseToken => {
|
|
|
483
703
|
/**
|
|
484
704
|
* Consume a purchase (Android only)
|
|
485
705
|
* @param purchaseToken - The purchase token to consume
|
|
706
|
+
* @returns Promise<boolean> - Indicates whether the consumption succeeded
|
|
486
707
|
*
|
|
487
|
-
* @example
|
|
488
|
-
* ```typescript
|
|
489
|
-
* await consumePurchaseAndroid('purchase_token_here');
|
|
490
|
-
* ```
|
|
491
|
-
*/
|
|
492
|
-
export const consumePurchaseAndroid = async purchaseToken => {
|
|
493
|
-
try {
|
|
494
|
-
if (Platform.OS !== 'android') {
|
|
495
|
-
throw new Error('consumePurchaseAndroid is only available on Android');
|
|
496
|
-
}
|
|
497
|
-
const result = await IAP.instance.finishTransaction({
|
|
498
|
-
android: {
|
|
499
|
-
purchaseToken,
|
|
500
|
-
isConsumable: true
|
|
501
|
-
}
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
// Result is a variant, extract PurchaseResult
|
|
505
|
-
if (typeof result === 'boolean') {
|
|
506
|
-
// This shouldn't happen for Android, but handle it
|
|
507
|
-
return {
|
|
508
|
-
responseCode: 0,
|
|
509
|
-
code: '0',
|
|
510
|
-
message: 'Success',
|
|
511
|
-
purchaseToken
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
return result;
|
|
515
|
-
} catch (error) {
|
|
516
|
-
console.error('Failed to consume purchase Android:', error);
|
|
517
|
-
throw error;
|
|
518
|
-
}
|
|
519
|
-
};
|
|
520
|
-
|
|
521
|
-
// ============================================================================
|
|
522
|
-
// EVENT LISTENERS
|
|
523
|
-
// ============================================================================
|
|
524
|
-
|
|
525
|
-
// Store wrapped listeners for proper removal
|
|
526
|
-
const purchaseUpdatedListenerMap = new WeakMap();
|
|
527
|
-
const purchaseErrorListenerMap = new WeakMap();
|
|
528
|
-
const promotedProductListenerMap = new WeakMap();
|
|
529
|
-
|
|
530
|
-
/**
|
|
531
|
-
* Purchase updated event listener
|
|
532
|
-
* Fired when a purchase is successful or when a pending purchase is completed.
|
|
533
|
-
*
|
|
534
|
-
* @param listener - Function to call when a purchase is updated
|
|
535
|
-
* @returns EventSubscription object with remove method
|
|
536
|
-
*
|
|
537
|
-
* @example
|
|
538
|
-
* ```typescript
|
|
539
|
-
* const subscription = purchaseUpdatedListener((purchase) => {
|
|
540
|
-
* console.log('Purchase successful:', purchase);
|
|
541
|
-
* // 1. Validate receipt with backend
|
|
542
|
-
* // 2. Deliver content to user
|
|
543
|
-
* // 3. Call finishTransaction to acknowledge
|
|
544
|
-
* });
|
|
545
|
-
*
|
|
546
|
-
* // Later, clean up
|
|
547
|
-
* subscription.remove();
|
|
548
|
-
* ```
|
|
549
|
-
*/
|
|
550
|
-
export const purchaseUpdatedListener = listener => {
|
|
551
|
-
// Wrap the listener to convert NitroPurchase to Purchase
|
|
552
|
-
const wrappedListener = nitroPurchase => {
|
|
553
|
-
if (validateNitroPurchase(nitroPurchase)) {
|
|
554
|
-
const convertedPurchase = convertNitroPurchaseToPurchase(nitroPurchase);
|
|
555
|
-
listener(convertedPurchase);
|
|
556
|
-
} else {
|
|
557
|
-
console.error('Invalid purchase data received from native:', nitroPurchase);
|
|
558
|
-
}
|
|
559
|
-
};
|
|
560
|
-
|
|
561
|
-
// Store the wrapped listener for removal
|
|
562
|
-
purchaseUpdatedListenerMap.set(listener, wrappedListener);
|
|
563
|
-
let attached = false;
|
|
564
|
-
try {
|
|
565
|
-
IAP.instance.addPurchaseUpdatedListener(wrappedListener);
|
|
566
|
-
attached = true;
|
|
567
|
-
} catch (e) {
|
|
568
|
-
const msg = toErrorMessage(e);
|
|
569
|
-
if (msg.includes('Nitro runtime not installed')) {
|
|
570
|
-
console.warn('[purchaseUpdatedListener] Nitro not ready yet; listener inert until initConnection()');
|
|
571
|
-
} else {
|
|
572
|
-
throw e;
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
return {
|
|
576
|
-
remove: () => {
|
|
577
|
-
const wrapped = purchaseUpdatedListenerMap.get(listener);
|
|
578
|
-
if (wrapped) {
|
|
579
|
-
if (attached) {
|
|
580
|
-
try {
|
|
581
|
-
IAP.instance.removePurchaseUpdatedListener(wrapped);
|
|
582
|
-
} catch {}
|
|
583
|
-
}
|
|
584
|
-
purchaseUpdatedListenerMap.delete(listener);
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
};
|
|
588
|
-
};
|
|
589
|
-
|
|
590
|
-
/**
|
|
591
|
-
* Purchase error event listener
|
|
592
|
-
* Fired when a purchase fails or is cancelled by the user.
|
|
593
|
-
*
|
|
594
|
-
* @param listener - Function to call when a purchase error occurs
|
|
595
|
-
* @returns EventSubscription object with remove method
|
|
596
|
-
*
|
|
597
|
-
* @example
|
|
598
|
-
* ```typescript
|
|
599
|
-
* const subscription = purchaseErrorListener((error) => {
|
|
600
|
-
* switch (error.code) {
|
|
601
|
-
* case 'E_USER_CANCELLED':
|
|
602
|
-
* // User cancelled - no action needed
|
|
603
|
-
* break;
|
|
604
|
-
* case 'E_ITEM_UNAVAILABLE':
|
|
605
|
-
* // Product not available
|
|
606
|
-
* break;
|
|
607
|
-
* case 'E_NETWORK_ERROR':
|
|
608
|
-
* // Retry with backoff
|
|
609
|
-
* break;
|
|
610
|
-
* }
|
|
611
|
-
* });
|
|
612
|
-
*
|
|
613
|
-
* // Later, clean up
|
|
614
|
-
* subscription.remove();
|
|
615
|
-
* ```
|
|
616
|
-
*/
|
|
617
|
-
export const purchaseErrorListener = listener => {
|
|
618
|
-
const wrapped = error => {
|
|
619
|
-
listener({
|
|
620
|
-
code: normalizeErrorCodeFromNative(error.code),
|
|
621
|
-
message: error.message,
|
|
622
|
-
productId: undefined
|
|
623
|
-
});
|
|
624
|
-
};
|
|
625
|
-
purchaseErrorListenerMap.set(listener, wrapped);
|
|
626
|
-
let attached = false;
|
|
627
|
-
try {
|
|
628
|
-
IAP.instance.addPurchaseErrorListener(wrapped);
|
|
629
|
-
attached = true;
|
|
630
|
-
} catch (e) {
|
|
631
|
-
const msg = toErrorMessage(e);
|
|
632
|
-
if (msg.includes('Nitro runtime not installed')) {
|
|
633
|
-
console.warn('[purchaseErrorListener] Nitro not ready yet; listener inert until initConnection()');
|
|
634
|
-
} else {
|
|
635
|
-
throw e;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
return {
|
|
639
|
-
remove: () => {
|
|
640
|
-
const stored = purchaseErrorListenerMap.get(listener);
|
|
641
|
-
if (stored) {
|
|
642
|
-
if (attached) {
|
|
643
|
-
try {
|
|
644
|
-
IAP.instance.removePurchaseErrorListener(stored);
|
|
645
|
-
} catch {}
|
|
646
|
-
}
|
|
647
|
-
purchaseErrorListenerMap.delete(listener);
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
};
|
|
651
|
-
};
|
|
652
|
-
|
|
653
|
-
/**
|
|
654
|
-
* iOS-only listener for App Store promoted product events.
|
|
655
|
-
* Fired when a user clicks on a promoted in-app purchase in the App Store.
|
|
656
|
-
*
|
|
657
|
-
* @param listener - Callback function that receives the promoted product
|
|
658
|
-
* @returns EventSubscription object with remove method
|
|
659
|
-
*
|
|
660
|
-
* @example
|
|
661
|
-
* ```typescript
|
|
662
|
-
* const subscription = promotedProductListenerIOS((product) => {
|
|
663
|
-
* console.log('Promoted product:', product);
|
|
664
|
-
* // Trigger purchase flow for the promoted product
|
|
665
|
-
* });
|
|
666
|
-
*
|
|
667
|
-
* // Later, clean up
|
|
668
|
-
* subscription.remove();
|
|
669
|
-
* ```
|
|
670
|
-
*
|
|
671
|
-
* @platform iOS
|
|
672
|
-
*/
|
|
673
|
-
export const promotedProductListenerIOS = listener => {
|
|
674
|
-
if (Platform.OS !== 'ios') {
|
|
675
|
-
console.warn('promotedProductListenerIOS: This listener is only available on iOS');
|
|
676
|
-
return {
|
|
677
|
-
remove: () => {}
|
|
678
|
-
};
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// Wrap the listener to convert NitroProduct to Product
|
|
682
|
-
const wrappedListener = nitroProduct => {
|
|
683
|
-
if (validateNitroProduct(nitroProduct)) {
|
|
684
|
-
const convertedProduct = convertNitroProductToProduct(nitroProduct);
|
|
685
|
-
listener(convertedProduct);
|
|
686
|
-
} else {
|
|
687
|
-
console.error('Invalid promoted product data received from native:', nitroProduct);
|
|
688
|
-
}
|
|
689
|
-
};
|
|
690
|
-
|
|
691
|
-
// Store the wrapped listener for removal
|
|
692
|
-
promotedProductListenerMap.set(listener, wrappedListener);
|
|
693
|
-
let attached = false;
|
|
694
|
-
try {
|
|
695
|
-
IAP.instance.addPromotedProductListenerIOS(wrappedListener);
|
|
696
|
-
attached = true;
|
|
697
|
-
} catch (e) {
|
|
698
|
-
const msg = toErrorMessage(e);
|
|
699
|
-
if (msg.includes('Nitro runtime not installed')) {
|
|
700
|
-
console.warn('[promotedProductListenerIOS] Nitro not ready yet; listener inert until initConnection()');
|
|
701
|
-
} else {
|
|
702
|
-
throw e;
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
return {
|
|
706
|
-
remove: () => {
|
|
707
|
-
const wrapped = promotedProductListenerMap.get(listener);
|
|
708
|
-
if (wrapped) {
|
|
709
|
-
if (attached) {
|
|
710
|
-
try {
|
|
711
|
-
IAP.instance.removePromotedProductListenerIOS(wrapped);
|
|
712
|
-
} catch {}
|
|
713
|
-
}
|
|
714
|
-
promotedProductListenerMap.delete(listener);
|
|
715
|
-
}
|
|
708
|
+
* @example
|
|
709
|
+
* ```typescript
|
|
710
|
+
* await consumePurchaseAndroid('purchase_token_here');
|
|
711
|
+
* ```
|
|
712
|
+
*/
|
|
713
|
+
export const consumePurchaseAndroid = async purchaseToken => {
|
|
714
|
+
try {
|
|
715
|
+
if (Platform.OS !== 'android') {
|
|
716
|
+
throw new Error('consumePurchaseAndroid is only available on Android');
|
|
716
717
|
}
|
|
717
|
-
|
|
718
|
+
const result = await IAP.instance.finishTransaction({
|
|
719
|
+
android: {
|
|
720
|
+
purchaseToken,
|
|
721
|
+
isConsumable: true
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
return getSuccessFromPurchaseVariant(result, 'consumePurchaseAndroid');
|
|
725
|
+
} catch (error) {
|
|
726
|
+
console.error('Failed to consume purchase Android:', error);
|
|
727
|
+
throw error;
|
|
728
|
+
}
|
|
718
729
|
};
|
|
719
730
|
|
|
720
731
|
// ============================================================================
|
|
@@ -727,11 +738,19 @@ export const promotedProductListenerIOS = listener => {
|
|
|
727
738
|
* @param androidOptions - Android-specific validation options (required for Android)
|
|
728
739
|
* @returns Promise<ReceiptValidationResultIOS | ReceiptValidationResultAndroid> - Platform-specific receipt validation result
|
|
729
740
|
*/
|
|
730
|
-
export const validateReceipt = async
|
|
741
|
+
export const validateReceipt = async options => {
|
|
742
|
+
const {
|
|
743
|
+
sku,
|
|
744
|
+
androidOptions
|
|
745
|
+
} = options;
|
|
731
746
|
try {
|
|
747
|
+
const normalizedAndroidOptions = androidOptions != null ? {
|
|
748
|
+
...androidOptions,
|
|
749
|
+
isSub: androidOptions.isSub == null ? undefined : Boolean(androidOptions.isSub)
|
|
750
|
+
} : undefined;
|
|
732
751
|
const params = {
|
|
733
752
|
sku,
|
|
734
|
-
androidOptions
|
|
753
|
+
androidOptions: normalizedAndroidOptions
|
|
735
754
|
};
|
|
736
755
|
const nitroResult = await IAP.instance.validateReceipt(params);
|
|
737
756
|
|
|
@@ -787,7 +806,8 @@ export const syncIOS = async () => {
|
|
|
787
806
|
throw new Error('syncIOS is only available on iOS');
|
|
788
807
|
}
|
|
789
808
|
try {
|
|
790
|
-
|
|
809
|
+
const result = await IAP.instance.syncIOS();
|
|
810
|
+
return Boolean(result);
|
|
791
811
|
} catch (error) {
|
|
792
812
|
console.error('[syncIOS] Failed:', error);
|
|
793
813
|
const errorJson = parseErrorStringToJsonObj(error);
|
|
@@ -795,31 +815,9 @@ export const syncIOS = async () => {
|
|
|
795
815
|
}
|
|
796
816
|
};
|
|
797
817
|
|
|
798
|
-
/**
|
|
799
|
-
* Request the promoted product from the App Store (iOS only)
|
|
800
|
-
* @returns Promise<Product | null> - The promoted product or null if none available
|
|
801
|
-
* @platform iOS
|
|
802
|
-
*/
|
|
803
|
-
export const requestPromotedProductIOS = async () => {
|
|
804
|
-
if (Platform.OS !== 'ios') {
|
|
805
|
-
return null;
|
|
806
|
-
}
|
|
807
|
-
try {
|
|
808
|
-
const nitroProduct = await IAP.instance.requestPromotedProductIOS();
|
|
809
|
-
if (nitroProduct) {
|
|
810
|
-
return convertNitroProductToProduct(nitroProduct);
|
|
811
|
-
}
|
|
812
|
-
return null;
|
|
813
|
-
} catch (error) {
|
|
814
|
-
console.error('[getPromotedProductIOS] Failed:', error);
|
|
815
|
-
const errorJson = parseErrorStringToJsonObj(error);
|
|
816
|
-
throw new Error(errorJson.message);
|
|
817
|
-
}
|
|
818
|
-
};
|
|
819
|
-
|
|
820
818
|
/**
|
|
821
819
|
* Present the code redemption sheet for offer codes (iOS only)
|
|
822
|
-
* @returns Promise<boolean> -
|
|
820
|
+
* @returns Promise<boolean> - Indicates whether the redemption sheet was presented
|
|
823
821
|
* @platform iOS
|
|
824
822
|
*/
|
|
825
823
|
export const presentCodeRedemptionSheetIOS = async () => {
|
|
@@ -827,7 +825,8 @@ export const presentCodeRedemptionSheetIOS = async () => {
|
|
|
827
825
|
return false;
|
|
828
826
|
}
|
|
829
827
|
try {
|
|
830
|
-
|
|
828
|
+
const result = await IAP.instance.presentCodeRedemptionSheetIOS();
|
|
829
|
+
return Boolean(result);
|
|
831
830
|
} catch (error) {
|
|
832
831
|
console.error('[presentCodeRedemptionSheetIOS] Failed:', error);
|
|
833
832
|
const errorJson = parseErrorStringToJsonObj(error);
|
|
@@ -837,33 +836,43 @@ export const presentCodeRedemptionSheetIOS = async () => {
|
|
|
837
836
|
|
|
838
837
|
/**
|
|
839
838
|
* Buy promoted product on iOS
|
|
840
|
-
* @returns Promise<
|
|
839
|
+
* @returns Promise<boolean> - true when the request triggers successfully
|
|
841
840
|
* @platform iOS
|
|
842
841
|
*/
|
|
843
|
-
export const
|
|
842
|
+
export const requestPurchaseOnPromotedProductIOS = async () => {
|
|
844
843
|
if (Platform.OS !== 'ios') {
|
|
845
|
-
throw new Error('
|
|
844
|
+
throw new Error('requestPurchaseOnPromotedProductIOS is only available on iOS');
|
|
846
845
|
}
|
|
847
846
|
try {
|
|
848
847
|
await IAP.instance.buyPromotedProductIOS();
|
|
848
|
+
const pending = await IAP.instance.getPendingTransactionsIOS();
|
|
849
|
+
const latest = pending.find(purchase => purchase != null);
|
|
850
|
+
if (!latest) {
|
|
851
|
+
throw new Error('No promoted purchase available after request');
|
|
852
|
+
}
|
|
853
|
+
const converted = convertNitroPurchaseToPurchase(latest);
|
|
854
|
+
if (converted.platform !== 'ios') {
|
|
855
|
+
throw new Error('Promoted purchase result not available for iOS');
|
|
856
|
+
}
|
|
857
|
+
return true;
|
|
849
858
|
} catch (error) {
|
|
850
|
-
console.error('[
|
|
851
|
-
|
|
852
|
-
throw new Error(errorJson.message);
|
|
859
|
+
console.error('[requestPurchaseOnPromotedProductIOS] Failed:', error);
|
|
860
|
+
throw error;
|
|
853
861
|
}
|
|
854
862
|
};
|
|
855
863
|
|
|
856
864
|
/**
|
|
857
865
|
* Clear unfinished transactions on iOS
|
|
858
|
-
* @returns Promise<
|
|
866
|
+
* @returns Promise<boolean>
|
|
859
867
|
* @platform iOS
|
|
860
868
|
*/
|
|
861
869
|
export const clearTransactionIOS = async () => {
|
|
862
870
|
if (Platform.OS !== 'ios') {
|
|
863
|
-
return;
|
|
871
|
+
return false;
|
|
864
872
|
}
|
|
865
873
|
try {
|
|
866
874
|
await IAP.instance.clearTransactionIOS();
|
|
875
|
+
return true;
|
|
867
876
|
} catch (error) {
|
|
868
877
|
console.error('[clearTransactionIOS] Failed:', error);
|
|
869
878
|
const errorJson = parseErrorStringToJsonObj(error);
|
|
@@ -879,10 +888,11 @@ export const clearTransactionIOS = async () => {
|
|
|
879
888
|
*/
|
|
880
889
|
export const beginRefundRequestIOS = async sku => {
|
|
881
890
|
if (Platform.OS !== 'ios') {
|
|
882
|
-
|
|
891
|
+
throw new Error('beginRefundRequestIOS is only available on iOS');
|
|
883
892
|
}
|
|
884
893
|
try {
|
|
885
|
-
|
|
894
|
+
const status = await IAP.instance.beginRefundRequestIOS(sku);
|
|
895
|
+
return status ?? null;
|
|
886
896
|
} catch (error) {
|
|
887
897
|
console.error('[beginRefundRequestIOS] Failed:', error);
|
|
888
898
|
const errorJson = parseErrorStringToJsonObj(error);
|
|
@@ -897,180 +907,51 @@ export const beginRefundRequestIOS = async sku => {
|
|
|
897
907
|
* @throws Error when called on non-iOS platforms or when IAP is not initialized
|
|
898
908
|
* @platform iOS
|
|
899
909
|
*/
|
|
900
|
-
export const subscriptionStatusIOS = async sku => {
|
|
901
|
-
if (Platform.OS !== 'ios') {
|
|
902
|
-
throw new Error('subscriptionStatusIOS is only available on iOS');
|
|
903
|
-
}
|
|
904
|
-
try {
|
|
905
|
-
const statuses = await IAP.instance.subscriptionStatusIOS(sku);
|
|
906
|
-
if (!Array.isArray(statuses)) return [];
|
|
907
|
-
return statuses.filter(status => status != null).map(convertNitroSubscriptionStatusToSubscriptionStatusIOS);
|
|
908
|
-
} catch (error) {
|
|
909
|
-
console.error('[subscriptionStatusIOS] Failed:', error);
|
|
910
|
-
const errorJson = parseErrorStringToJsonObj(error);
|
|
911
|
-
throw new Error(errorJson.message);
|
|
912
|
-
}
|
|
913
|
-
};
|
|
914
|
-
|
|
915
910
|
/**
|
|
916
911
|
* Get current entitlement for a product (iOS only)
|
|
917
912
|
* @param sku - The product SKU
|
|
918
913
|
* @returns Promise<Purchase | null> - Current entitlement or null
|
|
919
914
|
* @platform iOS
|
|
920
915
|
*/
|
|
921
|
-
export const currentEntitlementIOS = async sku => {
|
|
922
|
-
if (Platform.OS !== 'ios') {
|
|
923
|
-
return null;
|
|
924
|
-
}
|
|
925
|
-
try {
|
|
926
|
-
const nitroPurchase = await IAP.instance.currentEntitlementIOS(sku);
|
|
927
|
-
if (nitroPurchase) {
|
|
928
|
-
return convertNitroPurchaseToPurchase(nitroPurchase);
|
|
929
|
-
}
|
|
930
|
-
return null;
|
|
931
|
-
} catch (error) {
|
|
932
|
-
console.error('[currentEntitlementIOS] Failed:', error);
|
|
933
|
-
const errorJson = parseErrorStringToJsonObj(error);
|
|
934
|
-
throw new Error(errorJson.message);
|
|
935
|
-
}
|
|
936
|
-
};
|
|
937
|
-
|
|
938
916
|
/**
|
|
939
917
|
* Get latest transaction for a product (iOS only)
|
|
940
918
|
* @param sku - The product SKU
|
|
941
919
|
* @returns Promise<Purchase | null> - Latest transaction or null
|
|
942
920
|
* @platform iOS
|
|
943
921
|
*/
|
|
944
|
-
export const latestTransactionIOS = async sku => {
|
|
945
|
-
if (Platform.OS !== 'ios') {
|
|
946
|
-
return null;
|
|
947
|
-
}
|
|
948
|
-
try {
|
|
949
|
-
const nitroPurchase = await IAP.instance.latestTransactionIOS(sku);
|
|
950
|
-
if (nitroPurchase) {
|
|
951
|
-
return convertNitroPurchaseToPurchase(nitroPurchase);
|
|
952
|
-
}
|
|
953
|
-
return null;
|
|
954
|
-
} catch (error) {
|
|
955
|
-
console.error('[latestTransactionIOS] Failed:', error);
|
|
956
|
-
const errorJson = parseErrorStringToJsonObj(error);
|
|
957
|
-
throw new Error(errorJson.message);
|
|
958
|
-
}
|
|
959
|
-
};
|
|
960
|
-
|
|
961
922
|
/**
|
|
962
923
|
* Get pending transactions (iOS only)
|
|
963
924
|
* @returns Promise<Purchase[]> - Array of pending transactions
|
|
964
925
|
* @platform iOS
|
|
965
926
|
*/
|
|
966
|
-
export const getPendingTransactionsIOS = async () => {
|
|
967
|
-
if (Platform.OS !== 'ios') {
|
|
968
|
-
return [];
|
|
969
|
-
}
|
|
970
|
-
try {
|
|
971
|
-
const nitroPurchases = await IAP.instance.getPendingTransactionsIOS();
|
|
972
|
-
return nitroPurchases.map(convertNitroPurchaseToPurchase);
|
|
973
|
-
} catch (error) {
|
|
974
|
-
console.error('[getPendingTransactionsIOS] Failed:', error);
|
|
975
|
-
const errorJson = parseErrorStringToJsonObj(error);
|
|
976
|
-
throw new Error(errorJson.message);
|
|
977
|
-
}
|
|
978
|
-
};
|
|
979
|
-
|
|
980
927
|
/**
|
|
981
928
|
* Show manage subscriptions screen (iOS only)
|
|
982
929
|
* @returns Promise<Purchase[]> - Subscriptions where auto-renewal status changed
|
|
983
930
|
* @platform iOS
|
|
984
931
|
*/
|
|
985
|
-
export const showManageSubscriptionsIOS = async () => {
|
|
986
|
-
if (Platform.OS !== 'ios') {
|
|
987
|
-
return [];
|
|
988
|
-
}
|
|
989
|
-
try {
|
|
990
|
-
const nitroPurchases = await IAP.instance.showManageSubscriptionsIOS();
|
|
991
|
-
return nitroPurchases.map(convertNitroPurchaseToPurchase);
|
|
992
|
-
} catch (error) {
|
|
993
|
-
console.error('[showManageSubscriptionsIOS] Failed:', error);
|
|
994
|
-
const errorJson = parseErrorStringToJsonObj(error);
|
|
995
|
-
throw new Error(errorJson.message);
|
|
996
|
-
}
|
|
997
|
-
};
|
|
998
|
-
|
|
999
932
|
/**
|
|
1000
933
|
* Check if user is eligible for intro offer (iOS only)
|
|
1001
934
|
* @param groupID - The subscription group ID
|
|
1002
935
|
* @returns Promise<boolean> - Eligibility status
|
|
1003
936
|
* @platform iOS
|
|
1004
937
|
*/
|
|
1005
|
-
export const isEligibleForIntroOfferIOS = async groupID => {
|
|
1006
|
-
if (Platform.OS !== 'ios') {
|
|
1007
|
-
return false;
|
|
1008
|
-
}
|
|
1009
|
-
try {
|
|
1010
|
-
return await IAP.instance.isEligibleForIntroOfferIOS(groupID);
|
|
1011
|
-
} catch (error) {
|
|
1012
|
-
console.error('[isEligibleForIntroOfferIOS] Failed:', error);
|
|
1013
|
-
const errorJson = parseErrorStringToJsonObj(error);
|
|
1014
|
-
throw new Error(errorJson.message);
|
|
1015
|
-
}
|
|
1016
|
-
};
|
|
1017
|
-
|
|
1018
938
|
/**
|
|
1019
939
|
* Get receipt data (iOS only)
|
|
1020
940
|
* @returns Promise<string> - Base64 encoded receipt data
|
|
1021
941
|
* @platform iOS
|
|
1022
942
|
*/
|
|
1023
|
-
export const getReceiptDataIOS = async () => {
|
|
1024
|
-
if (Platform.OS !== 'ios') {
|
|
1025
|
-
throw new Error('getReceiptDataIOS is only available on iOS');
|
|
1026
|
-
}
|
|
1027
|
-
try {
|
|
1028
|
-
return await IAP.instance.getReceiptDataIOS();
|
|
1029
|
-
} catch (error) {
|
|
1030
|
-
console.error('[getReceiptDataIOS] Failed:', error);
|
|
1031
|
-
const errorJson = parseErrorStringToJsonObj(error);
|
|
1032
|
-
throw new Error(errorJson.message);
|
|
1033
|
-
}
|
|
1034
|
-
};
|
|
1035
|
-
|
|
1036
943
|
/**
|
|
1037
944
|
* Check if transaction is verified (iOS only)
|
|
1038
945
|
* @param sku - The product SKU
|
|
1039
946
|
* @returns Promise<boolean> - Verification status
|
|
1040
947
|
* @platform iOS
|
|
1041
948
|
*/
|
|
1042
|
-
export const isTransactionVerifiedIOS = async sku => {
|
|
1043
|
-
if (Platform.OS !== 'ios') {
|
|
1044
|
-
return false;
|
|
1045
|
-
}
|
|
1046
|
-
try {
|
|
1047
|
-
return await IAP.instance.isTransactionVerifiedIOS(sku);
|
|
1048
|
-
} catch (error) {
|
|
1049
|
-
console.error('[isTransactionVerifiedIOS] Failed:', error);
|
|
1050
|
-
const errorJson = parseErrorStringToJsonObj(error);
|
|
1051
|
-
throw new Error(errorJson.message);
|
|
1052
|
-
}
|
|
1053
|
-
};
|
|
1054
|
-
|
|
1055
949
|
/**
|
|
1056
950
|
* Get transaction JWS representation (iOS only)
|
|
1057
951
|
* @param sku - The product SKU
|
|
1058
952
|
* @returns Promise<string | null> - JWS representation or null
|
|
1059
953
|
* @platform iOS
|
|
1060
954
|
*/
|
|
1061
|
-
export const getTransactionJwsIOS = async sku => {
|
|
1062
|
-
if (Platform.OS !== 'ios') {
|
|
1063
|
-
return null;
|
|
1064
|
-
}
|
|
1065
|
-
try {
|
|
1066
|
-
return await IAP.instance.getTransactionJwsIOS(sku);
|
|
1067
|
-
} catch (error) {
|
|
1068
|
-
console.error('[getTransactionJwsIOS] Failed:', error);
|
|
1069
|
-
const errorJson = parseErrorStringToJsonObj(error);
|
|
1070
|
-
throw new Error(errorJson.message);
|
|
1071
|
-
}
|
|
1072
|
-
};
|
|
1073
|
-
|
|
1074
955
|
/**
|
|
1075
956
|
* Get the storefront identifier for the user's App Store account (iOS only)
|
|
1076
957
|
* @returns Promise<string> - The storefront identifier (e.g., 'USA' for United States)
|
|
@@ -1082,61 +963,26 @@ export const getTransactionJwsIOS = async sku => {
|
|
|
1082
963
|
* console.log('User storefront:', storefront); // e.g., 'USA', 'GBR', 'KOR'
|
|
1083
964
|
* ```
|
|
1084
965
|
*/
|
|
1085
|
-
export const getStorefrontIOS = async () => {
|
|
1086
|
-
if (Platform.OS !== 'ios') {
|
|
1087
|
-
throw new Error('getStorefrontIOS is only available on iOS');
|
|
1088
|
-
}
|
|
1089
|
-
try {
|
|
1090
|
-
// Call the native method to get storefront
|
|
1091
|
-
const storefront = await IAP.instance.getStorefrontIOS();
|
|
1092
|
-
return storefront;
|
|
1093
|
-
} catch (error) {
|
|
1094
|
-
console.error('Failed to get storefront:', error);
|
|
1095
|
-
throw error;
|
|
1096
|
-
}
|
|
1097
|
-
};
|
|
1098
|
-
|
|
1099
|
-
/**
|
|
1100
|
-
* Gets the storefront country code from the underlying native store.
|
|
1101
|
-
* Returns a two-letter country code such as 'US', 'KR', or empty string on failure.
|
|
1102
|
-
*
|
|
1103
|
-
* Cross-platform alias aligning with expo-iap.
|
|
1104
|
-
*/
|
|
1105
|
-
export const getStorefront = async () => {
|
|
1106
|
-
if (Platform.OS === 'android') {
|
|
1107
|
-
try {
|
|
1108
|
-
// Optional since older builds may not have the method
|
|
1109
|
-
const result = await IAP.instance.getStorefrontAndroid?.();
|
|
1110
|
-
return result ?? '';
|
|
1111
|
-
} catch {
|
|
1112
|
-
return '';
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
return getStorefrontIOS();
|
|
1116
|
-
};
|
|
1117
|
-
|
|
1118
966
|
/**
|
|
1119
967
|
* Deeplinks to native interface that allows users to manage their subscriptions
|
|
1120
968
|
* Cross-platform alias aligning with expo-iap
|
|
1121
969
|
*/
|
|
1122
|
-
export const deepLinkToSubscriptions = async
|
|
970
|
+
export const deepLinkToSubscriptions = async options => {
|
|
971
|
+
const resolvedOptions = options ?? undefined;
|
|
1123
972
|
if (Platform.OS === 'android') {
|
|
1124
973
|
await IAP.instance.deepLinkToSubscriptionsAndroid?.({
|
|
1125
|
-
skuAndroid:
|
|
1126
|
-
packageNameAndroid:
|
|
974
|
+
skuAndroid: resolvedOptions?.skuAndroid ?? undefined,
|
|
975
|
+
packageNameAndroid: resolvedOptions?.packageNameAndroid ?? undefined
|
|
1127
976
|
});
|
|
1128
977
|
return;
|
|
1129
978
|
}
|
|
1130
|
-
// iOS: Use manage subscriptions sheet (ignore returned purchases for deeplink parity)
|
|
1131
979
|
if (Platform.OS === 'ios') {
|
|
1132
980
|
try {
|
|
1133
981
|
await IAP.instance.showManageSubscriptionsIOS();
|
|
1134
|
-
} catch {
|
|
1135
|
-
|
|
982
|
+
} catch (error) {
|
|
983
|
+
console.warn('[deepLinkToSubscriptions] Failed on iOS:', error);
|
|
1136
984
|
}
|
|
1137
|
-
return;
|
|
1138
985
|
}
|
|
1139
|
-
return;
|
|
1140
986
|
};
|
|
1141
987
|
|
|
1142
988
|
/**
|
|
@@ -1158,20 +1004,6 @@ export const deepLinkToSubscriptions = async (options = {}) => {
|
|
|
1158
1004
|
* }
|
|
1159
1005
|
* ```
|
|
1160
1006
|
*/
|
|
1161
|
-
export const getAppTransactionIOS = async () => {
|
|
1162
|
-
if (Platform.OS !== 'ios') {
|
|
1163
|
-
throw new Error('getAppTransactionIOS is only available on iOS');
|
|
1164
|
-
}
|
|
1165
|
-
try {
|
|
1166
|
-
// Call the native method to get app transaction
|
|
1167
|
-
const appTransaction = await IAP.instance.getAppTransactionIOS();
|
|
1168
|
-
return appTransaction;
|
|
1169
|
-
} catch (error) {
|
|
1170
|
-
console.error('Failed to get app transaction:', error);
|
|
1171
|
-
throw error;
|
|
1172
|
-
}
|
|
1173
|
-
};
|
|
1174
|
-
|
|
1175
1007
|
// Export subscription helpers
|
|
1176
1008
|
export { getActiveSubscriptions, hasActiveSubscriptions } from "./helpers/subscription.js";
|
|
1177
1009
|
|
|
@@ -1188,4 +1020,57 @@ export const acknowledgePurchase = acknowledgePurchaseAndroid;
|
|
|
1188
1020
|
* @deprecated Use consumePurchaseAndroid instead
|
|
1189
1021
|
*/
|
|
1190
1022
|
export const consumePurchase = consumePurchaseAndroid;
|
|
1023
|
+
|
|
1024
|
+
// ============================================================================
|
|
1025
|
+
// Internal Helpers
|
|
1026
|
+
// ============================================================================
|
|
1027
|
+
|
|
1028
|
+
const toDiscountOfferRecordIOS = offer => {
|
|
1029
|
+
if (!offer) {
|
|
1030
|
+
return undefined;
|
|
1031
|
+
}
|
|
1032
|
+
return {
|
|
1033
|
+
identifier: offer.identifier,
|
|
1034
|
+
keyIdentifier: offer.keyIdentifier,
|
|
1035
|
+
nonce: offer.nonce,
|
|
1036
|
+
signature: offer.signature,
|
|
1037
|
+
timestamp: String(offer.timestamp)
|
|
1038
|
+
};
|
|
1039
|
+
};
|
|
1040
|
+
const toNitroProductType = type => {
|
|
1041
|
+
if (type === 'subs') {
|
|
1042
|
+
return 'subs';
|
|
1043
|
+
}
|
|
1044
|
+
if (type === 'all') {
|
|
1045
|
+
return 'all';
|
|
1046
|
+
}
|
|
1047
|
+
if (type === 'inapp') {
|
|
1048
|
+
console.warn(LEGACY_INAPP_WARNING);
|
|
1049
|
+
return 'inapp';
|
|
1050
|
+
}
|
|
1051
|
+
return 'inapp';
|
|
1052
|
+
};
|
|
1053
|
+
const isSubscriptionQuery = type => type === 'subs';
|
|
1054
|
+
const normalizeProductQueryType = type => {
|
|
1055
|
+
if (type === 'all' || type === 'subs' || type === 'in-app') {
|
|
1056
|
+
return type;
|
|
1057
|
+
}
|
|
1058
|
+
if (typeof type === 'string') {
|
|
1059
|
+
const normalized = type.trim().toLowerCase().replace(/_/g, '-');
|
|
1060
|
+
if (normalized === 'all') {
|
|
1061
|
+
return 'all';
|
|
1062
|
+
}
|
|
1063
|
+
if (normalized === 'subs') {
|
|
1064
|
+
return 'subs';
|
|
1065
|
+
}
|
|
1066
|
+
if (normalized === 'inapp') {
|
|
1067
|
+
console.warn(LEGACY_INAPP_WARNING);
|
|
1068
|
+
return 'in-app';
|
|
1069
|
+
}
|
|
1070
|
+
if (normalized === 'in-app') {
|
|
1071
|
+
return 'in-app';
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return 'in-app';
|
|
1075
|
+
};
|
|
1191
1076
|
//# sourceMappingURL=index.js.map
|