react-native-iap 16.4.1 → 16.5.0
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/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +200 -142
- package/android/src/main/java/com/margelo/nitro/iap/TokenizedListenerRegistry.kt +0 -1
- package/ios/HybridRnIap.swift +832 -165
- package/ios/RnIapHelper.swift +1 -1
- package/lib/module/hooks/useIAP.js +48 -32
- package/lib/module/hooks/useIAP.js.map +1 -1
- package/lib/module/index.js +206 -152
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.kepler.js +1 -0
- package/lib/module/index.kepler.js.map +1 -1
- package/lib/module/types.js +1 -0
- package/lib/module/types.js.map +1 -1
- package/lib/module/utils/error.js +5 -4
- package/lib/module/utils/error.js.map +1 -1
- package/lib/module/vega-adapter.js +4 -1
- package/lib/module/vega-adapter.js.map +1 -1
- package/lib/typescript/src/hooks/useIAP.d.ts +31 -15
- package/lib/typescript/src/hooks/useIAP.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +28 -14
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/index.kepler.d.ts.map +1 -1
- package/lib/typescript/src/specs/RnIap.nitro.d.ts +10 -0
- package/lib/typescript/src/specs/RnIap.nitro.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +11 -2
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/utils/error.d.ts.map +1 -1
- package/lib/typescript/src/vega-adapter.d.ts.map +1 -1
- package/nitrogen/generated/android/NitroIap+autolinking.cmake +1 -0
- package/nitrogen/generated/android/c++/JHybridRnIapSpec.cpp +9 -0
- package/nitrogen/generated/android/c++/JNitroPurchaseRequest.hpp +7 -1
- package/nitrogen/generated/android/c++/JNitroPurchaseRequestType.hpp +58 -0
- package/nitrogen/generated/android/c++/JNitroVerifyPurchaseWithIapkitHorizonProps.hpp +66 -0
- package/nitrogen/generated/android/c++/JNitroVerifyPurchaseWithIapkitProps.hpp +8 -1
- package/nitrogen/generated/android/c++/JNitroVerifyPurchaseWithProviderProps.hpp +3 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_NitroVerifyPurchaseWithIapkitHorizonProps.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_NitroVerifyPurchaseWithIapkitHorizonProps.hpp +74 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_NitroVerifyPurchaseWithIapkitProps.hpp +3 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroPurchaseRequest.kt +8 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroPurchaseRequestType.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroVerifyPurchaseWithIapkitHorizonProps.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroVerifyPurchaseWithIapkitProps.kt +7 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/Variant_NullType_NitroVerifyPurchaseWithIapkitHorizonProps.kt +62 -0
- package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Bridge.hpp +65 -0
- package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Umbrella.hpp +6 -0
- package/nitrogen/generated/ios/c++/HybridRnIapSpecSwift.hpp +6 -0
- package/nitrogen/generated/ios/swift/NitroPurchaseRequest.swift +13 -2
- package/nitrogen/generated/ios/swift/NitroPurchaseRequestType.swift +40 -0
- package/nitrogen/generated/ios/swift/NitroVerifyPurchaseWithIapkitHorizonProps.swift +66 -0
- package/nitrogen/generated/ios/swift/NitroVerifyPurchaseWithIapkitProps.swift +38 -1
- package/nitrogen/generated/ios/swift/Variant_NullType_NitroVerifyPurchaseWithIapkitHorizonProps.swift +30 -0
- package/nitrogen/generated/shared/c++/NitroPurchaseRequest.hpp +9 -2
- package/nitrogen/generated/shared/c++/NitroPurchaseRequestType.hpp +76 -0
- package/nitrogen/generated/shared/c++/NitroVerifyPurchaseWithIapkitHorizonProps.hpp +90 -0
- package/nitrogen/generated/shared/c++/NitroVerifyPurchaseWithIapkitProps.hpp +8 -1
- package/openiap-versions.json +3 -3
- package/package.json +1 -1
- package/src/hooks/useIAP.ts +96 -58
- package/src/index.kepler.ts +1 -0
- package/src/index.ts +248 -195
- package/src/specs/RnIap.nitro.ts +12 -0
- package/src/types.ts +12 -2
- package/src/utils/error.ts +7 -3
- package/src/vega-adapter.ts +9 -5
package/lib/module/index.js
CHANGED
|
@@ -56,6 +56,7 @@ export { kitApi, KitApiError } from "./kit-api.js";
|
|
|
56
56
|
|
|
57
57
|
// Create the RnIap HybridObject instance lazily to avoid early JSI crashes
|
|
58
58
|
let iapRef = null;
|
|
59
|
+
let attachingPendingNativeListeners = false;
|
|
59
60
|
|
|
60
61
|
/**
|
|
61
62
|
* Check if Nitro runtime is ready for IAP operations.
|
|
@@ -101,29 +102,41 @@ export const isStandardIOS = () => {
|
|
|
101
102
|
const isAndroidStoreRuntime = () => {
|
|
102
103
|
return Platform.OS === 'android' || isVegaOS();
|
|
103
104
|
};
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
throw new Error('Amazon Vega IAP module is unavailable. Install @amazon-devices/keplerscript-appstore-iap-lib in the Vega app target and build with the React Native for Vega kepler platform.');
|
|
111
|
-
}
|
|
112
|
-
iapRef = vegaModule;
|
|
113
|
-
return iapRef;
|
|
105
|
+
function getRawIapInstance() {
|
|
106
|
+
if (iapRef) return iapRef;
|
|
107
|
+
if (isVegaOS()) {
|
|
108
|
+
const vegaModule = getVegaIapModule();
|
|
109
|
+
if (!vegaModule) {
|
|
110
|
+
throw new Error('Amazon Vega IAP module is unavailable. Install @amazon-devices/keplerscript-appstore-iap-lib in the Vega app target and build with the React Native for Vega kepler platform.');
|
|
114
111
|
}
|
|
112
|
+
iapRef = vegaModule;
|
|
113
|
+
return iapRef;
|
|
114
|
+
}
|
|
115
115
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
116
|
+
// Attempt to create the HybridObject and map common Nitro/JSI readiness errors
|
|
117
|
+
try {
|
|
118
|
+
iapRef = NitroModules.createHybridObject('RnIap');
|
|
119
|
+
} catch (e) {
|
|
120
|
+
const msg = toErrorMessage(e);
|
|
121
|
+
if (msg.includes('Nitro') || msg.includes('JSI') || msg.includes('dispatcher') || msg.includes('HybridObject')) {
|
|
122
|
+
throw new Error('Nitro runtime not installed yet. Ensure react-native-nitro-modules is initialized before calling IAP.');
|
|
123
|
+
}
|
|
124
|
+
throw e;
|
|
125
|
+
}
|
|
126
|
+
return iapRef;
|
|
127
|
+
}
|
|
128
|
+
const IAP = {
|
|
129
|
+
get instance() {
|
|
130
|
+
const instance = getRawIapInstance();
|
|
131
|
+
if (!attachingPendingNativeListeners) {
|
|
132
|
+
attachingPendingNativeListeners = true;
|
|
133
|
+
try {
|
|
134
|
+
tryAttachPendingNativeListeners();
|
|
135
|
+
} finally {
|
|
136
|
+
attachingPendingNativeListeners = false;
|
|
123
137
|
}
|
|
124
|
-
throw e;
|
|
125
138
|
}
|
|
126
|
-
return
|
|
139
|
+
return instance;
|
|
127
140
|
}
|
|
128
141
|
};
|
|
129
142
|
|
|
@@ -135,6 +148,17 @@ const IAP = {
|
|
|
135
148
|
// hyodotdev/openiap#382.
|
|
136
149
|
// ============================================================================
|
|
137
150
|
|
|
151
|
+
function attachNativeListenerOrDefer(label, attach) {
|
|
152
|
+
try {
|
|
153
|
+
attach();
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (toErrorMessage(error).includes('Nitro runtime not installed')) {
|
|
156
|
+
RnIapConsole.warn(`[${label}] Nitro not ready yet; will retry on the next native operation`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
138
162
|
const purchaseUpdateJsListeners = new Set();
|
|
139
163
|
const purchaseUpdateDuplicateJsListeners = new Set();
|
|
140
164
|
let purchaseUpdateNativeAttached = false;
|
|
@@ -161,6 +185,23 @@ const purchaseUpdateNativeHandler = nitroPurchase => {
|
|
|
161
185
|
const purchaseUpdateDuplicateNativeHandler = nitroPurchase => {
|
|
162
186
|
emitPurchaseUpdateToListeners(nitroPurchase, purchaseUpdateDuplicateJsListeners);
|
|
163
187
|
};
|
|
188
|
+
function tryAttachPurchaseUpdateNative(receiveDuplicateTransactionUpdatesIOS) {
|
|
189
|
+
const alreadyAttached = receiveDuplicateTransactionUpdatesIOS ? purchaseUpdateDuplicateNativeAttached : purchaseUpdateNativeAttached;
|
|
190
|
+
if (alreadyAttached) return;
|
|
191
|
+
attachNativeListenerOrDefer('purchaseUpdatedListener', () => {
|
|
192
|
+
const nativeOptions = receiveDuplicateTransactionUpdatesIOS ? {
|
|
193
|
+
dedupeTransactionIOS: false
|
|
194
|
+
} : undefined;
|
|
195
|
+
const token = getRawIapInstance().addPurchaseUpdatedListener(receiveDuplicateTransactionUpdatesIOS ? purchaseUpdateDuplicateNativeHandler : purchaseUpdateNativeHandler, nativeOptions);
|
|
196
|
+
if (receiveDuplicateTransactionUpdatesIOS) {
|
|
197
|
+
purchaseUpdateDuplicateNativeToken = typeof token === 'number' ? token : null;
|
|
198
|
+
purchaseUpdateDuplicateNativeAttached = true;
|
|
199
|
+
} else {
|
|
200
|
+
purchaseUpdateNativeToken = typeof token === 'number' ? token : null;
|
|
201
|
+
purchaseUpdateNativeAttached = true;
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
164
205
|
const purchaseErrorJsListeners = new Set();
|
|
165
206
|
let purchaseErrorNativeAttached = false;
|
|
166
207
|
const purchaseErrorNativeHandler = error => {
|
|
@@ -184,6 +225,13 @@ const purchaseErrorNativeHandler = error => {
|
|
|
184
225
|
}
|
|
185
226
|
}
|
|
186
227
|
};
|
|
228
|
+
function tryAttachPurchaseErrorNative() {
|
|
229
|
+
if (purchaseErrorNativeAttached) return;
|
|
230
|
+
attachNativeListenerOrDefer('purchaseErrorListener', () => {
|
|
231
|
+
getRawIapInstance().addPurchaseErrorListener(purchaseErrorNativeHandler);
|
|
232
|
+
purchaseErrorNativeAttached = true;
|
|
233
|
+
});
|
|
234
|
+
}
|
|
187
235
|
const promotedProductJsListeners = new Set();
|
|
188
236
|
let promotedProductNativeAttached = false;
|
|
189
237
|
const promotedProductNativeHandler = nitroProduct => {
|
|
@@ -200,6 +248,13 @@ const promotedProductNativeHandler = nitroProduct => {
|
|
|
200
248
|
RnIapConsole.error('Invalid promoted product data received from native — id:', nitroProduct?.id ?? 'unknown');
|
|
201
249
|
}
|
|
202
250
|
};
|
|
251
|
+
function tryAttachPromotedProductNative() {
|
|
252
|
+
if (promotedProductNativeAttached) return;
|
|
253
|
+
attachNativeListenerOrDefer('promotedProductListenerIOS', () => {
|
|
254
|
+
getRawIapInstance().addPromotedProductListenerIOS(promotedProductNativeHandler);
|
|
255
|
+
promotedProductNativeAttached = true;
|
|
256
|
+
});
|
|
257
|
+
}
|
|
203
258
|
|
|
204
259
|
/**
|
|
205
260
|
* Reset all JS-level listener tracking state.
|
|
@@ -228,36 +283,11 @@ export const purchaseUpdatedListener = (listener, options) => {
|
|
|
228
283
|
const receiveDuplicateTransactionUpdatesIOS = Platform.OS === 'ios' && options?.dedupeTransactionIOS === false;
|
|
229
284
|
const listeners = receiveDuplicateTransactionUpdatesIOS ? purchaseUpdateDuplicateJsListeners : purchaseUpdateJsListeners;
|
|
230
285
|
listeners.add(listener);
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
} catch (e) {
|
|
237
|
-
const msg = toErrorMessage(e);
|
|
238
|
-
if (msg.includes('Nitro runtime not installed')) {
|
|
239
|
-
RnIapConsole.warn('[purchaseUpdatedListener] Nitro not ready yet; listener inert until initConnection()');
|
|
240
|
-
} else {
|
|
241
|
-
throw e;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
if (!purchaseUpdateDuplicateNativeAttached && receiveDuplicateTransactionUpdatesIOS) {
|
|
246
|
-
try {
|
|
247
|
-
const nativeOptions = {
|
|
248
|
-
dedupeTransactionIOS: false
|
|
249
|
-
};
|
|
250
|
-
const token = IAP.instance.addPurchaseUpdatedListener(purchaseUpdateDuplicateNativeHandler, nativeOptions);
|
|
251
|
-
purchaseUpdateDuplicateNativeToken = typeof token === 'number' ? token : null;
|
|
252
|
-
purchaseUpdateDuplicateNativeAttached = true;
|
|
253
|
-
} catch (e) {
|
|
254
|
-
const msg = toErrorMessage(e);
|
|
255
|
-
if (msg.includes('Nitro runtime not installed')) {
|
|
256
|
-
RnIapConsole.warn('[purchaseUpdatedListener] Nitro not ready yet; listener inert until initConnection()');
|
|
257
|
-
} else {
|
|
258
|
-
throw e;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
286
|
+
try {
|
|
287
|
+
tryAttachPurchaseUpdateNative(receiveDuplicateTransactionUpdatesIOS);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
listeners.delete(listener);
|
|
290
|
+
throw error;
|
|
261
291
|
}
|
|
262
292
|
let removed = false;
|
|
263
293
|
return {
|
|
@@ -282,7 +312,7 @@ export const purchaseUpdatedListener = (listener, options) => {
|
|
|
282
312
|
return;
|
|
283
313
|
}
|
|
284
314
|
try {
|
|
285
|
-
|
|
315
|
+
getRawIapInstance().removePurchaseUpdatedListener(token);
|
|
286
316
|
if (receiveDuplicateTransactionUpdatesIOS) {
|
|
287
317
|
purchaseUpdateDuplicateNativeToken = null;
|
|
288
318
|
purchaseUpdateDuplicateNativeAttached = false;
|
|
@@ -298,18 +328,11 @@ export const purchaseUpdatedListener = (listener, options) => {
|
|
|
298
328
|
};
|
|
299
329
|
export const purchaseErrorListener = listener => {
|
|
300
330
|
purchaseErrorJsListeners.add(listener);
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const msg = toErrorMessage(e);
|
|
307
|
-
if (msg.includes('Nitro runtime not installed')) {
|
|
308
|
-
RnIapConsole.warn('[purchaseErrorListener] Nitro not ready yet; listener inert until initConnection()');
|
|
309
|
-
} else {
|
|
310
|
-
throw e;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
331
|
+
try {
|
|
332
|
+
tryAttachPurchaseErrorNative();
|
|
333
|
+
} catch (error) {
|
|
334
|
+
purchaseErrorJsListeners.delete(listener);
|
|
335
|
+
throw error;
|
|
313
336
|
}
|
|
314
337
|
return {
|
|
315
338
|
remove: () => {
|
|
@@ -327,7 +350,7 @@ export const purchaseErrorListener = listener => {
|
|
|
327
350
|
}
|
|
328
351
|
if (purchaseErrorNativeAttached) {
|
|
329
352
|
try {
|
|
330
|
-
|
|
353
|
+
getRawIapInstance().removePurchaseErrorListener(purchaseErrorNativeHandler);
|
|
331
354
|
purchaseErrorNativeAttached = false;
|
|
332
355
|
} catch (e) {
|
|
333
356
|
RnIapConsole.warn('[purchaseErrorListener] native remove failed:', e);
|
|
@@ -352,18 +375,11 @@ export const promotedProductListenerIOS = listener => {
|
|
|
352
375
|
};
|
|
353
376
|
}
|
|
354
377
|
promotedProductJsListeners.add(listener);
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const msg = toErrorMessage(e);
|
|
361
|
-
if (msg.includes('Nitro runtime not installed')) {
|
|
362
|
-
RnIapConsole.warn('[promotedProductListenerIOS] Nitro not ready yet; listener inert until initConnection()');
|
|
363
|
-
} else {
|
|
364
|
-
throw e;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
378
|
+
try {
|
|
379
|
+
tryAttachPromotedProductNative();
|
|
380
|
+
} catch (error) {
|
|
381
|
+
promotedProductJsListeners.delete(listener);
|
|
382
|
+
throw error;
|
|
367
383
|
}
|
|
368
384
|
return {
|
|
369
385
|
remove: () => {
|
|
@@ -388,7 +404,9 @@ export const promotedProductListenerIOS = listener => {
|
|
|
388
404
|
* console.log('External transaction token received; send it to your backend without logging it.');
|
|
389
405
|
*
|
|
390
406
|
* // Send token to backend for Google Play reporting
|
|
391
|
-
*
|
|
407
|
+
* void reportToGooglePlay(details.externalTransactionToken).catch((error) => {
|
|
408
|
+
* console.warn('Alternative billing report failed', error);
|
|
409
|
+
* });
|
|
392
410
|
* });
|
|
393
411
|
*
|
|
394
412
|
* // Later, remove the listener
|
|
@@ -407,6 +425,13 @@ const userChoiceBillingNativeHandler = details => {
|
|
|
407
425
|
}
|
|
408
426
|
}
|
|
409
427
|
};
|
|
428
|
+
function tryAttachUserChoiceBillingNative() {
|
|
429
|
+
if (userChoiceBillingNativeAttached) return;
|
|
430
|
+
attachNativeListenerOrDefer('userChoiceBillingListenerAndroid', () => {
|
|
431
|
+
getRawIapInstance().addUserChoiceBillingListenerAndroid(userChoiceBillingNativeHandler);
|
|
432
|
+
userChoiceBillingNativeAttached = true;
|
|
433
|
+
});
|
|
434
|
+
}
|
|
410
435
|
export const userChoiceBillingListenerAndroid = listener => {
|
|
411
436
|
if (Platform.OS !== 'android') {
|
|
412
437
|
RnIapConsole.warn('userChoiceBillingListenerAndroid: This listener is only available on Android');
|
|
@@ -415,18 +440,11 @@ export const userChoiceBillingListenerAndroid = listener => {
|
|
|
415
440
|
};
|
|
416
441
|
}
|
|
417
442
|
userChoiceBillingJsListeners.add(listener);
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const msg = toErrorMessage(e);
|
|
424
|
-
if (msg.includes('Nitro runtime not installed')) {
|
|
425
|
-
RnIapConsole.warn('[userChoiceBillingListenerAndroid] Nitro not ready yet; listener inert until initConnection()');
|
|
426
|
-
} else {
|
|
427
|
-
throw e;
|
|
428
|
-
}
|
|
429
|
-
}
|
|
443
|
+
try {
|
|
444
|
+
tryAttachUserChoiceBillingNative();
|
|
445
|
+
} catch (error) {
|
|
446
|
+
userChoiceBillingJsListeners.delete(listener);
|
|
447
|
+
throw error;
|
|
430
448
|
}
|
|
431
449
|
let removed = false;
|
|
432
450
|
return {
|
|
@@ -438,7 +456,7 @@ export const userChoiceBillingListenerAndroid = listener => {
|
|
|
438
456
|
return;
|
|
439
457
|
}
|
|
440
458
|
try {
|
|
441
|
-
|
|
459
|
+
getRawIapInstance().removeUserChoiceBillingListenerAndroid(userChoiceBillingNativeHandler);
|
|
442
460
|
userChoiceBillingNativeAttached = false;
|
|
443
461
|
} catch (e) {
|
|
444
462
|
RnIapConsole.warn('[userChoiceBillingListenerAndroid] native remove failed:', e);
|
|
@@ -462,10 +480,11 @@ export const userChoiceBillingListenerAndroid = listener => {
|
|
|
462
480
|
* @example
|
|
463
481
|
* ```typescript
|
|
464
482
|
* const subscription = developerProvidedBillingListenerAndroid((details) => {
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
483
|
+
* void processExternalPayment(details.products, details.linkUri)
|
|
484
|
+
* .then(() => details.externalTransactionToken
|
|
485
|
+
* ? reportToGooglePlay(details.externalTransactionToken)
|
|
486
|
+
* : undefined)
|
|
487
|
+
* .catch((error) => console.warn('Developer billing failed', error));
|
|
469
488
|
* });
|
|
470
489
|
*
|
|
471
490
|
* // Later, remove the listener
|
|
@@ -484,6 +503,13 @@ const developerProvidedBillingNativeHandler = details => {
|
|
|
484
503
|
}
|
|
485
504
|
}
|
|
486
505
|
};
|
|
506
|
+
function tryAttachDeveloperProvidedBillingNative() {
|
|
507
|
+
if (developerProvidedBillingNativeAttached) return;
|
|
508
|
+
attachNativeListenerOrDefer('developerProvidedBillingListenerAndroid', () => {
|
|
509
|
+
getRawIapInstance().addDeveloperProvidedBillingListenerAndroid(developerProvidedBillingNativeHandler);
|
|
510
|
+
developerProvidedBillingNativeAttached = true;
|
|
511
|
+
});
|
|
512
|
+
}
|
|
487
513
|
export const developerProvidedBillingListenerAndroid = listener => {
|
|
488
514
|
if (Platform.OS !== 'android') {
|
|
489
515
|
RnIapConsole.warn('developerProvidedBillingListenerAndroid: This listener is only available on Android');
|
|
@@ -492,18 +518,11 @@ export const developerProvidedBillingListenerAndroid = listener => {
|
|
|
492
518
|
};
|
|
493
519
|
}
|
|
494
520
|
developerProvidedBillingJsListeners.add(listener);
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
const msg = toErrorMessage(e);
|
|
501
|
-
if (msg.includes('Nitro runtime not installed')) {
|
|
502
|
-
RnIapConsole.warn('[developerProvidedBillingListenerAndroid] Nitro not ready yet; listener inert until initConnection()');
|
|
503
|
-
} else {
|
|
504
|
-
throw e;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
521
|
+
try {
|
|
522
|
+
tryAttachDeveloperProvidedBillingNative();
|
|
523
|
+
} catch (error) {
|
|
524
|
+
developerProvidedBillingJsListeners.delete(listener);
|
|
525
|
+
throw error;
|
|
507
526
|
}
|
|
508
527
|
return {
|
|
509
528
|
remove: () => {
|
|
@@ -555,17 +574,10 @@ const subscriptionBillingIssueNativeHandler = nitroPurchase => {
|
|
|
555
574
|
};
|
|
556
575
|
function tryAttachSubscriptionBillingIssueNative() {
|
|
557
576
|
if (subscriptionBillingIssueNativeAttached) return;
|
|
558
|
-
|
|
559
|
-
|
|
577
|
+
attachNativeListenerOrDefer('subscriptionBillingIssueListener', () => {
|
|
578
|
+
getRawIapInstance().addSubscriptionBillingIssueListener(subscriptionBillingIssueNativeHandler);
|
|
560
579
|
subscriptionBillingIssueNativeAttached = true;
|
|
561
|
-
}
|
|
562
|
-
const msg = toErrorMessage(e);
|
|
563
|
-
if (msg.includes('Nitro runtime not installed')) {
|
|
564
|
-
RnIapConsole.warn('[subscriptionBillingIssueListener] Nitro not ready yet; will retry after initConnection()');
|
|
565
|
-
} else {
|
|
566
|
-
throw e;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
580
|
+
});
|
|
569
581
|
}
|
|
570
582
|
export const subscriptionBillingIssueListener = listener => {
|
|
571
583
|
subscriptionBillingIssueJsListeners.add(listener);
|
|
@@ -583,6 +595,29 @@ export const subscriptionBillingIssueListener = listener => {
|
|
|
583
595
|
}
|
|
584
596
|
};
|
|
585
597
|
};
|
|
598
|
+
function tryAttachPendingNativeListeners() {
|
|
599
|
+
if (purchaseUpdateJsListeners.size > 0) {
|
|
600
|
+
tryAttachPurchaseUpdateNative(false);
|
|
601
|
+
}
|
|
602
|
+
if (purchaseUpdateDuplicateJsListeners.size > 0) {
|
|
603
|
+
tryAttachPurchaseUpdateNative(true);
|
|
604
|
+
}
|
|
605
|
+
if (purchaseErrorJsListeners.size > 0) {
|
|
606
|
+
tryAttachPurchaseErrorNative();
|
|
607
|
+
}
|
|
608
|
+
if (promotedProductJsListeners.size > 0) {
|
|
609
|
+
tryAttachPromotedProductNative();
|
|
610
|
+
}
|
|
611
|
+
if (userChoiceBillingJsListeners.size > 0) {
|
|
612
|
+
tryAttachUserChoiceBillingNative();
|
|
613
|
+
}
|
|
614
|
+
if (developerProvidedBillingJsListeners.size > 0) {
|
|
615
|
+
tryAttachDeveloperProvidedBillingNative();
|
|
616
|
+
}
|
|
617
|
+
if (subscriptionBillingIssueJsListeners.size > 0) {
|
|
618
|
+
tryAttachSubscriptionBillingIssueNative();
|
|
619
|
+
}
|
|
620
|
+
}
|
|
586
621
|
|
|
587
622
|
// ------------------------------
|
|
588
623
|
// Query API
|
|
@@ -852,8 +887,13 @@ export const getAppTransactionIOS = async () => {
|
|
|
852
887
|
}
|
|
853
888
|
return null;
|
|
854
889
|
} catch (error) {
|
|
855
|
-
|
|
856
|
-
throw
|
|
890
|
+
const parsedError = parseErrorAndLogIfNeeded('Failed to get app transaction:', error);
|
|
891
|
+
throw createPurchaseError({
|
|
892
|
+
code: parsedError.code,
|
|
893
|
+
message: parsedError.message,
|
|
894
|
+
responseCode: parsedError.responseCode,
|
|
895
|
+
debugMessage: parsedError.debugMessage
|
|
896
|
+
});
|
|
857
897
|
}
|
|
858
898
|
};
|
|
859
899
|
|
|
@@ -1152,25 +1192,29 @@ export const getTransactionJwsIOS = async sku => {
|
|
|
1152
1192
|
*
|
|
1153
1193
|
* @example
|
|
1154
1194
|
* ```ts
|
|
1155
|
-
*
|
|
1156
|
-
* await initConnection(
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
1159
|
-
*
|
|
1160
|
-
* });
|
|
1195
|
+
* // Choose exactly one connection call for the session.
|
|
1196
|
+
* const connected = await initConnection();
|
|
1197
|
+
* // Or replace the call above with one Android billing program:
|
|
1198
|
+
* // const connected = await initConnection({
|
|
1199
|
+
* // enableBillingProgramAndroid: 'external-offer',
|
|
1200
|
+
* // });
|
|
1201
|
+
* // const connected = await initConnection({
|
|
1202
|
+
* // enableBillingProgramAndroid: 'billing-choice',
|
|
1203
|
+
* // billingChoiceScreenTypeAndroid: 'developer-rendered',
|
|
1204
|
+
* // });
|
|
1205
|
+
* if (!connected) throw new Error('Store connection failed');
|
|
1161
1206
|
* ```
|
|
1162
1207
|
*
|
|
1163
|
-
* @remarks When using `useIAP()`, connection
|
|
1164
|
-
*
|
|
1208
|
+
* @remarks When using `useIAP()`, the connection initializes on mount. On unmount,
|
|
1209
|
+
* React Native removes hook listeners but keeps the native connection open across
|
|
1210
|
+
* screens. Pass options to the hook instead of calling this directly.
|
|
1165
1211
|
*
|
|
1166
1212
|
* @see {@link https://openiap.dev/docs/apis/init-connection}
|
|
1167
1213
|
*/
|
|
1168
1214
|
export const initConnection = async config => {
|
|
1169
1215
|
try {
|
|
1170
1216
|
const result = await IAP.instance.initConnection(config);
|
|
1171
|
-
|
|
1172
|
-
tryAttachSubscriptionBillingIssueNative();
|
|
1173
|
-
}
|
|
1217
|
+
tryAttachPendingNativeListeners();
|
|
1174
1218
|
return result;
|
|
1175
1219
|
} catch (error) {
|
|
1176
1220
|
const parsedError = parseErrorAndLogIfNeeded('Failed to initialize IAP connection:', error);
|
|
@@ -1190,8 +1234,9 @@ export const initConnection = async config => {
|
|
|
1190
1234
|
*/
|
|
1191
1235
|
export const endConnection = async () => {
|
|
1192
1236
|
try {
|
|
1193
|
-
|
|
1194
|
-
|
|
1237
|
+
const result = iapRef ? await IAP.instance.endConnection() : true;
|
|
1238
|
+
resetListenerState();
|
|
1239
|
+
return result;
|
|
1195
1240
|
} catch (error) {
|
|
1196
1241
|
const parsedError = parseErrorAndLogIfNeeded('Failed to end IAP connection:', error);
|
|
1197
1242
|
throw createPurchaseError({
|
|
@@ -1200,8 +1245,6 @@ export const endConnection = async () => {
|
|
|
1200
1245
|
responseCode: parsedError.responseCode,
|
|
1201
1246
|
debugMessage: parsedError.debugMessage
|
|
1202
1247
|
});
|
|
1203
|
-
} finally {
|
|
1204
|
-
resetListenerState();
|
|
1205
1248
|
}
|
|
1206
1249
|
};
|
|
1207
1250
|
|
|
@@ -1245,10 +1288,14 @@ export const restorePurchases = async () => {
|
|
|
1245
1288
|
* - `type: 'in-app'` — pass `request.apple.sku` (iOS) and/or `request.google.skus` (Android).
|
|
1246
1289
|
* - `type: 'subs'` — same shape, plus `request.google.subscriptionOffers: [{ sku, offerToken }]`.
|
|
1247
1290
|
* @returns The dispatched purchase payload. **Do not rely on it** for the actual outcome.
|
|
1248
|
-
* @throws
|
|
1291
|
+
* @throws Invalid arguments or failures that prevent dispatch. Store outcomes are event-based;
|
|
1292
|
+
* on Android, `not-prepared` is delivered through `purchaseErrorListener`.
|
|
1249
1293
|
*
|
|
1250
1294
|
* @example
|
|
1251
1295
|
* ```ts
|
|
1296
|
+
* const connected = await initConnection();
|
|
1297
|
+
* if (!connected) return;
|
|
1298
|
+
*
|
|
1252
1299
|
* await requestPurchase({
|
|
1253
1300
|
* request: {
|
|
1254
1301
|
* apple: { sku: 'com.app.premium' },
|
|
@@ -1301,7 +1348,9 @@ export const requestPurchase = async request => {
|
|
|
1301
1348
|
} else {
|
|
1302
1349
|
throw unsupportedPlatformError();
|
|
1303
1350
|
}
|
|
1304
|
-
const unifiedRequest = {
|
|
1351
|
+
const unifiedRequest = {
|
|
1352
|
+
type: normalizedType
|
|
1353
|
+
};
|
|
1305
1354
|
if (Platform.OS === 'ios' && iosRequestSource) {
|
|
1306
1355
|
const iosRequest = isSubs ? iosRequestSource : iosRequestSource;
|
|
1307
1356
|
const iosPayload = {
|
|
@@ -1429,10 +1478,12 @@ export const requestPurchase = async request => {
|
|
|
1429
1478
|
*
|
|
1430
1479
|
* @example
|
|
1431
1480
|
* ```ts
|
|
1432
|
-
* purchaseUpdatedListener(
|
|
1433
|
-
*
|
|
1434
|
-
*
|
|
1435
|
-
*
|
|
1481
|
+
* purchaseUpdatedListener((purchase) => {
|
|
1482
|
+
* void verifyOnServer(purchase)
|
|
1483
|
+
* .then((verified) => verified
|
|
1484
|
+
* ? finishTransaction({ purchase, isConsumable: false })
|
|
1485
|
+
* : undefined)
|
|
1486
|
+
* .catch((error) => console.warn('Transaction finalization failed:', error));
|
|
1436
1487
|
* });
|
|
1437
1488
|
* ```
|
|
1438
1489
|
*
|
|
@@ -1478,18 +1529,25 @@ export const finishTransaction = async args => {
|
|
|
1478
1529
|
}
|
|
1479
1530
|
return;
|
|
1480
1531
|
} catch (error) {
|
|
1532
|
+
const parsedError = parseErrorStringToJsonObj(error);
|
|
1481
1533
|
// If iOS transaction has already been auto-finished natively, treat as success
|
|
1482
1534
|
if (Platform.OS === 'ios') {
|
|
1483
|
-
const
|
|
1484
|
-
const
|
|
1485
|
-
const code = (err?.code || '').toString();
|
|
1535
|
+
const msg = (parsedError.message || '').toString();
|
|
1536
|
+
const code = (parsedError.code || '').toString();
|
|
1486
1537
|
if (msg.includes('Transaction not found') || code === 'E_ITEM_UNAVAILABLE') {
|
|
1487
1538
|
// Consider already finished
|
|
1488
1539
|
return;
|
|
1489
1540
|
}
|
|
1490
1541
|
}
|
|
1491
|
-
|
|
1492
|
-
|
|
1542
|
+
if (!isUserCancelledError(parsedError)) {
|
|
1543
|
+
RnIapConsole.error('Failed to finish transaction:', error);
|
|
1544
|
+
}
|
|
1545
|
+
throw createPurchaseError({
|
|
1546
|
+
code: parsedError.code,
|
|
1547
|
+
message: parsedError.message,
|
|
1548
|
+
responseCode: parsedError.responseCode,
|
|
1549
|
+
debugMessage: parsedError.debugMessage
|
|
1550
|
+
});
|
|
1493
1551
|
}
|
|
1494
1552
|
};
|
|
1495
1553
|
|
|
@@ -2060,18 +2118,13 @@ export const getActiveSubscriptions = async subscriptionIds => {
|
|
|
2060
2118
|
*
|
|
2061
2119
|
* @param subscriptionIds - Optional array of subscription IDs to check
|
|
2062
2120
|
* @returns Promise<boolean> - True if there are active subscriptions
|
|
2121
|
+
* @throws When the store cannot determine subscription status
|
|
2063
2122
|
*
|
|
2064
2123
|
* @see {@link https://openiap.dev/docs/apis/has-active-subscriptions}
|
|
2065
2124
|
*/
|
|
2066
2125
|
export const hasActiveSubscriptions = async subscriptionIds => {
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
return activeSubscriptions.length > 0;
|
|
2070
|
-
} catch (error) {
|
|
2071
|
-
// If there's an error getting subscriptions, return false
|
|
2072
|
-
RnIapConsole.warn('Error checking active subscriptions:', error);
|
|
2073
|
-
return false;
|
|
2074
|
-
}
|
|
2126
|
+
const activeSubscriptions = await getActiveSubscriptions(subscriptionIds);
|
|
2127
|
+
return activeSubscriptions.length > 0;
|
|
2075
2128
|
};
|
|
2076
2129
|
|
|
2077
2130
|
// Type conversion utilities
|
|
@@ -2128,7 +2181,8 @@ const normalizeProductQueryType = type => {
|
|
|
2128
2181
|
* ```typescript
|
|
2129
2182
|
* // Enable external offers before connecting
|
|
2130
2183
|
* enableBillingProgramAndroid('external-offer');
|
|
2131
|
-
* await initConnection();
|
|
2184
|
+
* const connected = await initConnection();
|
|
2185
|
+
* if (!connected) throw new Error('Store connection failed');
|
|
2132
2186
|
* ```
|
|
2133
2187
|
*/
|
|
2134
2188
|
export const enableBillingProgramAndroid = program => {
|