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