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