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