react-native-iap 14.3.0 → 14.3.2-rc.1

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.
@@ -60,12 +60,12 @@ class HybridRnIap: HybridRnIapSpec {
60
60
  #if DEBUG
61
61
  print("[HybridRnIap] purchaseError event: code=\(error.code), productId=\(error.productId ?? "-")")
62
62
  #endif
63
- let nitroError = self.createPurchaseErrorResult(
64
- code: error.code,
65
- message: error.message,
66
- productId: error.productId
67
- )
68
- self.sendPurchaseError(nitroError)
63
+ let nitroError = self.createPurchaseErrorResult(
64
+ code: error.code,
65
+ message: error.message,
66
+ productId: error.productId
67
+ )
68
+ self.sendPurchaseError(nitroError, productId: error.productId)
69
69
  }
70
70
  }
71
71
  }
@@ -123,7 +123,7 @@ class HybridRnIap: HybridRnIapSpec {
123
123
  message: error.message,
124
124
  productId: error.productId
125
125
  )
126
- self.sendPurchaseError(nitroError)
126
+ self.sendPurchaseError(nitroError, productId: error.productId)
127
127
  }
128
128
  }
129
129
  }
@@ -168,7 +168,7 @@ class HybridRnIap: HybridRnIapSpec {
168
168
  message: error.localizedDescription,
169
169
  productId: nil
170
170
  )
171
- self.sendPurchaseError(err)
171
+ self.sendPurchaseError(err, productId: nil)
172
172
  self.isInitialized = false
173
173
  self.isInitializing = false
174
174
  return false
@@ -205,7 +205,7 @@ class HybridRnIap: HybridRnIapSpec {
205
205
  code: OpenIapError.E_USER_ERROR,
206
206
  message: "No iOS request provided"
207
207
  )
208
- self.sendPurchaseError(error)
208
+ self.sendPurchaseError(error, productId: nil)
209
209
  return
210
210
  }
211
211
  do {
@@ -219,7 +219,7 @@ class HybridRnIap: HybridRnIapSpec {
219
219
  message: "IAP store connection not initialized",
220
220
  productId: iosRequest.sku
221
221
  )
222
- self.sendPurchaseError(err)
222
+ self.sendPurchaseError(err, productId: iosRequest.sku)
223
223
  return
224
224
  }
225
225
  // Delegate purchase to OpenIAP. It emits success/error events which we bridge above.
@@ -246,7 +246,7 @@ class HybridRnIap: HybridRnIapSpec {
246
246
  message: error.localizedDescription,
247
247
  productId: iosRequest.sku
248
248
  )
249
- self.sendPurchaseErrorDedup(err)
249
+ self.sendPurchaseErrorDedup(err, productId: iosRequest.sku)
250
250
  }
251
251
  }
252
252
  }
@@ -612,25 +612,30 @@ class HybridRnIap: HybridRnIapSpec {
612
612
  }
613
613
  }
614
614
 
615
- private func sendPurchaseError(_ error: NitroPurchaseResult) {
616
- // Update last error for deduplication
615
+ private func sendPurchaseError(_ error: NitroPurchaseResult, productId: String? = nil) {
616
+ // Update last error for deduplication using the associated product SKU (not token)
617
617
  lastPurchaseErrorCode = error.code
618
- lastPurchaseErrorProductId = error.purchaseToken
618
+ lastPurchaseErrorProductId = productId
619
619
  lastPurchaseErrorTimestamp = Date().timeIntervalSince1970
620
+ // Ensure we never leak SKU via purchaseToken
621
+ var sanitized = error
622
+ if let pid = productId, sanitized.purchaseToken == pid {
623
+ sanitized.purchaseToken = nil
624
+ }
620
625
  for listener in purchaseErrorListeners {
621
- listener(error)
626
+ listener(sanitized)
622
627
  }
623
628
  }
624
629
 
625
- private func sendPurchaseErrorDedup(_ error: NitroPurchaseResult) {
630
+ private func sendPurchaseErrorDedup(_ error: NitroPurchaseResult, productId: String? = nil) {
626
631
  let now = Date().timeIntervalSince1970
627
632
  let sameCode = (error.code == lastPurchaseErrorCode)
628
- let sameProduct = (error.purchaseToken == lastPurchaseErrorProductId)
633
+ let sameProduct = (productId == lastPurchaseErrorProductId)
629
634
  let withinWindow = (now - lastPurchaseErrorTimestamp) < 0.3
630
635
  if sameCode && sameProduct && withinWindow {
631
636
  return
632
637
  }
633
- sendPurchaseError(error)
638
+ sendPurchaseError(error, productId: productId)
634
639
  }
635
640
 
636
641
  private func createPurchaseErrorResult(code: String, message: String, productId: String? = nil) -> NitroPurchaseResult {
@@ -638,7 +643,8 @@ class HybridRnIap: HybridRnIapSpec {
638
643
  result.responseCode = 0
639
644
  result.code = code
640
645
  result.message = message
641
- result.purchaseToken = productId
646
+ // Do not overload the token field with productId
647
+ result.purchaseToken = nil
642
648
  return result
643
649
  }
644
650
 
package/lib/index.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from './specs/RnIap.nitro';
2
+ declare class RnIapImpl {
3
+ private hybridObject;
4
+ private getHybridObject;
5
+ toString(): string;
6
+ }
7
+ declare const RnIap: RnIapImpl;
8
+ export default RnIap;
package/lib/index.js ADDED
@@ -0,0 +1,36 @@
1
+ import { NitroModules } from 'react-native-nitro-modules';
2
+ export * from './specs/RnIap.nitro';
3
+ class RnIapImpl {
4
+ hybridObject = null;
5
+ getHybridObject() {
6
+ if (!this.hybridObject) {
7
+ try {
8
+ console.log('🔧 Creating RnIap HybridObject...');
9
+ this.hybridObject = NitroModules.createHybridObject('RnIap');
10
+ console.log('🔧 HybridObject created successfully:', !!this.hybridObject);
11
+ }
12
+ catch (error) {
13
+ console.error('🔧 Failed to create HybridObject:', error);
14
+ throw new Error(`Failed to create RnIap HybridObject: ${error}`);
15
+ }
16
+ }
17
+ return this.hybridObject;
18
+ }
19
+ toString() {
20
+ try {
21
+ console.log('🔧 Getting HybridObject for toString...');
22
+ const hybridObject = this.getHybridObject();
23
+ console.log('🔧 HybridObject obtained, calling toString...');
24
+ const result = hybridObject.toString();
25
+ console.log('🔧 toString completed with result:', result);
26
+ return result;
27
+ }
28
+ catch (error) {
29
+ console.error('🔧 toString failed:', error);
30
+ throw error;
31
+ }
32
+ }
33
+ }
34
+ // Create singleton instance
35
+ const RnIap = new RnIapImpl();
36
+ export default RnIap;
@@ -26,8 +26,8 @@ export const getActiveSubscriptions = async subscriptionIds => {
26
26
  isActive: true,
27
27
  // If it's in availablePurchases, it's active
28
28
  // Backend validation fields
29
- transactionId: purchase.transactionId || purchase.id,
30
- purchaseToken: androidPurchase.purchaseToken || androidPurchase.purchaseTokenAndroid || iosPurchase.purchaseToken,
29
+ transactionId: purchase.id,
30
+ purchaseToken: purchase.purchaseToken,
31
31
  transactionDate: purchase.transactionDate,
32
32
  // Platform-specific fields
33
33
  expirationDateIOS: iosPurchase.expirationDateIOS ? new Date(iosPurchase.expirationDateIOS) : undefined,
@@ -1 +1 @@
1
- {"version":3,"names":["getAvailablePurchases","getActiveSubscriptions","subscriptionIds","purchases","subscriptions","filter","purchase","length","includes","productId","map","iosPurchase","androidPurchase","isActive","transactionId","id","purchaseToken","purchaseTokenAndroid","transactionDate","expirationDateIOS","Date","undefined","autoRenewingAndroid","isAutoRenewing","environmentIOS","willExpireSoon","daysUntilExpirationIOS","Math","ceil","now","error","console","hasActiveSubscriptions","activeSubscriptions"],"sourceRoot":"../../../src","sources":["helpers/subscription.ts"],"mappings":";;AAAA,SAAQA,qBAAqB,QAAO,aAAK;AAGzC;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMC,sBAAsB,GAAG,MACpCC,eAA0B,IACQ;EAClC,IAAI;IACF;IACA,MAAMC,SAAS,GAAG,MAAMH,qBAAqB,CAAC,CAAC;;IAE/C;IACA,MAAMI,aAAa,GAAGD,SAAS,CAC5BE,MAAM,CAAEC,QAAQ,IAAK;MACpB;MACA,IAAIJ,eAAe,IAAIA,eAAe,CAACK,MAAM,GAAG,CAAC,EAAE;QACjD,OAAOL,eAAe,CAACM,QAAQ,CAACF,QAAQ,CAACG,SAAS,CAAC;MACrD;MACA,OAAO,IAAI;IACb,CAAC,CAAC,CACDC,GAAG,CAAEJ,QAAQ,IAAyB;MACrC,MAAMK,WAAW,GAAGL,QAAuB;MAC3C,MAAMM,eAAe,GAAGN,QAA2B;MACnD,OAAO;QACLG,SAAS,EAAEH,QAAQ,CAACG,SAAS;QAC7BI,QAAQ,EAAE,IAAI;QAAE;QAChB;QACAC,aAAa,EAAER,QAAQ,CAACQ,aAAa,IAAIR,QAAQ,CAACS,EAAE;QACpDC,aAAa,EACXJ,eAAe,CAACI,aAAa,IAC7BJ,eAAe,CAACK,oBAAoB,IACpCN,WAAW,CAACK,aAAa;QAC3BE,eAAe,EAAEZ,QAAQ,CAACY,eAAe;QACzC;QACAC,iBAAiB,EAAER,WAAW,CAACQ,iBAAiB,GAC5C,IAAIC,IAAI,CAACT,WAAW,CAACQ,iBAAiB,CAAC,GACvCE,SAAS;QACbC,mBAAmB,EACjBV,eAAe,CAACU,mBAAmB,IACnCV,eAAe,CAACW,cAAc;QAAE;QAClCC,cAAc,EAAEb,WAAW,CAACa,cAAc;QAC1C;QACAC,cAAc,EAAE,KAAK;QAAE;QACvBC,sBAAsB,EAAEf,WAAW,CAACQ,iBAAiB,GACjDQ,IAAI,CAACC,IAAI,CACP,CAACjB,WAAW,CAACQ,iBAAiB,GAAGC,IAAI,CAACS,GAAG,CAAC,CAAC,KACxC,IAAI,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CACxB,CAAC,GACDR;MACN,CAAC;IACH,CAAC,CAAC;IAEJ,OAAOjB,aAAa;EACtB,CAAC,CAAC,OAAO0B,KAAK,EAAE;IACdC,OAAO,CAACD,KAAK,CAAC,qCAAqC,EAAEA,KAAK,CAAC;IAC3D,MAAMA,KAAK;EACb;AACF,CAAC;;AAED;AACA;AACA;AACA;AACA;AACA,OAAO,MAAME,sBAAsB,GAAG,MACpC9B,eAA0B,IACL;EACrB,IAAI;IACF,MAAM+B,mBAAmB,GAAG,MAAMhC,sBAAsB,CAACC,eAAe,CAAC;IACzE,OAAO+B,mBAAmB,CAAC1B,MAAM,GAAG,CAAC;EACvC,CAAC,CAAC,OAAOuB,KAAK,EAAE;IACdC,OAAO,CAACD,KAAK,CAAC,uCAAuC,EAAEA,KAAK,CAAC;IAC7D,OAAO,KAAK;EACd;AACF,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["getAvailablePurchases","getActiveSubscriptions","subscriptionIds","purchases","subscriptions","filter","purchase","length","includes","productId","map","iosPurchase","androidPurchase","isActive","transactionId","id","purchaseToken","transactionDate","expirationDateIOS","Date","undefined","autoRenewingAndroid","isAutoRenewing","environmentIOS","willExpireSoon","daysUntilExpirationIOS","Math","ceil","now","error","console","hasActiveSubscriptions","activeSubscriptions"],"sourceRoot":"../../../src","sources":["helpers/subscription.ts"],"mappings":";;AAAA,SAAQA,qBAAqB,QAAO,aAAK;AAGzC;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMC,sBAAsB,GAAG,MACpCC,eAA0B,IACQ;EAClC,IAAI;IACF;IACA,MAAMC,SAAS,GAAG,MAAMH,qBAAqB,CAAC,CAAC;;IAE/C;IACA,MAAMI,aAAa,GAAGD,SAAS,CAC5BE,MAAM,CAAEC,QAAQ,IAAK;MACpB;MACA,IAAIJ,eAAe,IAAIA,eAAe,CAACK,MAAM,GAAG,CAAC,EAAE;QACjD,OAAOL,eAAe,CAACM,QAAQ,CAACF,QAAQ,CAACG,SAAS,CAAC;MACrD;MACA,OAAO,IAAI;IACb,CAAC,CAAC,CACDC,GAAG,CAAEJ,QAAQ,IAAyB;MACrC,MAAMK,WAAW,GAAGL,QAAuB;MAC3C,MAAMM,eAAe,GAAGN,QAA2B;MACnD,OAAO;QACLG,SAAS,EAAEH,QAAQ,CAACG,SAAS;QAC7BI,QAAQ,EAAE,IAAI;QAAE;QAChB;QACAC,aAAa,EAAER,QAAQ,CAACS,EAAE;QAC1BC,aAAa,EAAEV,QAAQ,CAACU,aAAa;QACrCC,eAAe,EAAEX,QAAQ,CAACW,eAAe;QACzC;QACAC,iBAAiB,EAAEP,WAAW,CAACO,iBAAiB,GAC5C,IAAIC,IAAI,CAACR,WAAW,CAACO,iBAAiB,CAAC,GACvCE,SAAS;QACbC,mBAAmB,EACjBT,eAAe,CAACS,mBAAmB,IACnCT,eAAe,CAACU,cAAc;QAAE;QAClCC,cAAc,EAAEZ,WAAW,CAACY,cAAc;QAC1C;QACAC,cAAc,EAAE,KAAK;QAAE;QACvBC,sBAAsB,EAAEd,WAAW,CAACO,iBAAiB,GACjDQ,IAAI,CAACC,IAAI,CACP,CAAChB,WAAW,CAACO,iBAAiB,GAAGC,IAAI,CAACS,GAAG,CAAC,CAAC,KACxC,IAAI,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CACxB,CAAC,GACDR;MACN,CAAC;IACH,CAAC,CAAC;IAEJ,OAAOhB,aAAa;EACtB,CAAC,CAAC,OAAOyB,KAAK,EAAE;IACdC,OAAO,CAACD,KAAK,CAAC,qCAAqC,EAAEA,KAAK,CAAC;IAC3D,MAAMA,KAAK;EACb;AACF,CAAC;;AAED;AACA;AACA;AACA;AACA;AACA,OAAO,MAAME,sBAAsB,GAAG,MACpC7B,eAA0B,IACL;EACrB,IAAI;IACF,MAAM8B,mBAAmB,GAAG,MAAM/B,sBAAsB,CAACC,eAAe,CAAC;IACzE,OAAO8B,mBAAmB,CAACzB,MAAM,GAAG,CAAC;EACvC,CAAC,CAAC,OAAOsB,KAAK,EAAE;IACdC,OAAO,CAACD,KAAK,CAAC,uCAAuC,EAAEA,KAAK,CAAC;IAC7D,OAAO,KAAK;EACd;AACF,CAAC","ignoreList":[]}
@@ -38,15 +38,27 @@ export const restorePurchases = async (options = {
38
38
 
39
39
  // Development utilities removed - use type bridge functions directly if needed
40
40
 
41
- // Create the RnIap HybridObject instance (internal use only)
42
- const iap = NitroModules.createHybridObject('RnIap');
41
+ // Create the RnIap HybridObject instance lazily to avoid early JSI crashes
42
+ let iap = null;
43
+ const getIap = () => {
44
+ if (iap) return iap;
45
+
46
+ // Guard against accessing Nitro before it's installed into the JS runtime
47
+ const hasNitroDispatcher = typeof globalThis !== 'undefined' && globalThis?.__nitro?.dispatcher != null;
48
+ const isJestEnvironment = typeof globalThis.jest !== 'undefined' || typeof process !== 'undefined' && !!process.env && process.env.JEST_WORKER_ID != null;
49
+ if (!hasNitroDispatcher && !isJestEnvironment) {
50
+ throw new Error('Nitro runtime not installed yet. Ensure react-native-nitro-modules is initialized before calling IAP.');
51
+ }
52
+ iap = NitroModules.createHybridObject('RnIap');
53
+ return iap;
54
+ };
43
55
 
44
56
  /**
45
57
  * Initialize connection to the store
46
58
  */
47
59
  export const initConnection = async () => {
48
60
  try {
49
- return await iap.initConnection();
61
+ return await getIap().initConnection();
50
62
  } catch (error) {
51
63
  console.error('Failed to initialize IAP connection:', error);
52
64
  throw error;
@@ -58,7 +70,9 @@ export const initConnection = async () => {
58
70
  */
59
71
  export const endConnection = async () => {
60
72
  try {
61
- return await iap.endConnection();
73
+ // If never initialized, treat as ended
74
+ if (!iap) return true;
75
+ return await getIap().endConnection();
62
76
  } catch (error) {
63
77
  console.error('Failed to end IAP connection:', error);
64
78
  throw error;
@@ -90,7 +104,7 @@ export const fetchProducts = async ({
90
104
  throw new Error('No SKUs provided');
91
105
  }
92
106
  if (type === 'all') {
93
- const [inappNitro, subsNitro] = await Promise.all([iap.fetchProducts(skus, 'inapp'), iap.fetchProducts(skus, 'subs')]);
107
+ const [inappNitro, subsNitro] = await Promise.all([getIap().fetchProducts(skus, 'inapp'), getIap().fetchProducts(skus, 'subs')]);
94
108
  const allNitro = [...inappNitro, ...subsNitro];
95
109
  const validAll = allNitro.filter(validateNitroProduct);
96
110
  if (validAll.length !== allNitro.length) {
@@ -98,7 +112,7 @@ export const fetchProducts = async ({
98
112
  }
99
113
  return validAll.map(convertNitroProductToProduct);
100
114
  }
101
- const nitroProducts = await iap.fetchProducts(skus, type);
115
+ const nitroProducts = await getIap().fetchProducts(skus, type);
102
116
 
103
117
  // Validate and convert NitroProducts to TypeScript Products
104
118
  const validProducts = nitroProducts.filter(validateNitroProduct);
@@ -197,7 +211,7 @@ export const requestPurchase = async ({
197
211
  }
198
212
 
199
213
  // Call unified method - returns void, listen for events instead
200
- await iap.requestPurchase(unifiedRequest);
214
+ await getIap().requestPurchase(unifiedRequest);
201
215
  } catch (error) {
202
216
  console.error('Failed to request purchase:', error);
203
217
  throw error;
@@ -234,12 +248,12 @@ export const getAvailablePurchases = async ({
234
248
  };
235
249
  } else if (Platform.OS === 'android') {
236
250
  // For Android, we need to call twice for inapp and subs
237
- const inappNitroPurchases = await iap.getAvailablePurchases({
251
+ const inappNitroPurchases = await getIap().getAvailablePurchases({
238
252
  android: {
239
253
  type: 'inapp'
240
254
  }
241
255
  });
242
- const subsNitroPurchases = await iap.getAvailablePurchases({
256
+ const subsNitroPurchases = await getIap().getAvailablePurchases({
243
257
  android: {
244
258
  type: 'subs'
245
259
  }
@@ -255,7 +269,7 @@ export const getAvailablePurchases = async ({
255
269
  } else {
256
270
  throw new Error('Unsupported platform');
257
271
  }
258
- const nitroPurchases = await iap.getAvailablePurchases(options);
272
+ const nitroPurchases = await getIap().getAvailablePurchases(options);
259
273
 
260
274
  // Validate and convert NitroPurchases to TypeScript Purchases
261
275
  const validPurchases = nitroPurchases.filter(validateNitroPurchase);
@@ -299,7 +313,7 @@ export const finishTransaction = async ({
299
313
  };
300
314
  } else if (Platform.OS === 'android') {
301
315
  const androidPurchase = purchase;
302
- const token = androidPurchase.purchaseToken || androidPurchase.purchaseTokenAndroid;
316
+ const token = androidPurchase.purchaseToken;
303
317
  if (!token) {
304
318
  throw new Error('purchaseToken required to finish Android transaction');
305
319
  }
@@ -310,7 +324,7 @@ export const finishTransaction = async ({
310
324
  } else {
311
325
  throw new Error('Unsupported platform');
312
326
  }
313
- const result = await iap.finishTransaction(params);
327
+ const result = await getIap().finishTransaction(params);
314
328
 
315
329
  // Handle variant return type
316
330
  if (typeof result === 'boolean') {
@@ -348,7 +362,7 @@ export const acknowledgePurchaseAndroid = async purchaseToken => {
348
362
  if (Platform.OS !== 'android') {
349
363
  throw new Error('acknowledgePurchaseAndroid is only available on Android');
350
364
  }
351
- const result = await iap.finishTransaction({
365
+ const result = await getIap().finishTransaction({
352
366
  android: {
353
367
  purchaseToken,
354
368
  isConsumable: false
@@ -386,7 +400,7 @@ export const consumePurchaseAndroid = async purchaseToken => {
386
400
  if (Platform.OS !== 'android') {
387
401
  throw new Error('consumePurchaseAndroid is only available on Android');
388
402
  }
389
- const result = await iap.finishTransaction({
403
+ const result = await getIap().finishTransaction({
390
404
  android: {
391
405
  purchaseToken,
392
406
  isConsumable: true
@@ -450,12 +464,27 @@ export const purchaseUpdatedListener = listener => {
450
464
 
451
465
  // Store the wrapped listener for removal
452
466
  listenerMap.set(listener, wrappedListener);
453
- iap.addPurchaseUpdatedListener(wrappedListener);
467
+ let attached = false;
468
+ try {
469
+ getIap().addPurchaseUpdatedListener(wrappedListener);
470
+ attached = true;
471
+ } catch (e) {
472
+ const msg = String(e ?? '');
473
+ if (msg.includes('Nitro runtime not installed')) {
474
+ console.warn('[purchaseUpdatedListener] Nitro not ready yet; listener inert until initConnection()');
475
+ } else {
476
+ throw e;
477
+ }
478
+ }
454
479
  return {
455
480
  remove: () => {
456
481
  const wrapped = listenerMap.get(listener);
457
482
  if (wrapped) {
458
- iap.removePurchaseUpdatedListener(wrapped);
483
+ if (attached) {
484
+ try {
485
+ getIap().removePurchaseUpdatedListener(wrapped);
486
+ } catch {}
487
+ }
459
488
  listenerMap.delete(listener);
460
489
  }
461
490
  }
@@ -492,10 +521,25 @@ export const purchaseUpdatedListener = listener => {
492
521
  export const purchaseErrorListener = listener => {
493
522
  // Store the listener for removal
494
523
  listenerMap.set(listener, listener);
495
- iap.addPurchaseErrorListener(listener);
524
+ let attached = false;
525
+ try {
526
+ getIap().addPurchaseErrorListener(listener);
527
+ attached = true;
528
+ } catch (e) {
529
+ const msg = String(e ?? '');
530
+ if (msg.includes('Nitro runtime not installed')) {
531
+ console.warn('[purchaseErrorListener] Nitro not ready yet; listener inert until initConnection()');
532
+ } else {
533
+ throw e;
534
+ }
535
+ }
496
536
  return {
497
537
  remove: () => {
498
- iap.removePurchaseErrorListener(listener);
538
+ if (attached) {
539
+ try {
540
+ getIap().removePurchaseErrorListener(listener);
541
+ } catch {}
542
+ }
499
543
  listenerMap.delete(listener);
500
544
  }
501
545
  };
@@ -541,12 +585,27 @@ export const promotedProductListenerIOS = listener => {
541
585
 
542
586
  // Store the wrapped listener for removal
543
587
  listenerMap.set(listener, wrappedListener);
544
- iap.addPromotedProductListenerIOS(wrappedListener);
588
+ let attached = false;
589
+ try {
590
+ getIap().addPromotedProductListenerIOS(wrappedListener);
591
+ attached = true;
592
+ } catch (e) {
593
+ const msg = String(e ?? '');
594
+ if (msg.includes('Nitro runtime not installed')) {
595
+ console.warn('[promotedProductListenerIOS] Nitro not ready yet; listener inert until initConnection()');
596
+ } else {
597
+ throw e;
598
+ }
599
+ }
545
600
  return {
546
601
  remove: () => {
547
602
  const wrapped = listenerMap.get(listener);
548
603
  if (wrapped) {
549
- iap.removePromotedProductListenerIOS(wrapped);
604
+ if (attached) {
605
+ try {
606
+ getIap().removePromotedProductListenerIOS(wrapped);
607
+ } catch {}
608
+ }
550
609
  listenerMap.delete(listener);
551
610
  }
552
611
  }
@@ -564,16 +623,12 @@ export const promotedProductListenerIOS = listener => {
564
623
  * @returns Promise<ReceiptValidationResultIOS | ReceiptValidationResultAndroid> - Platform-specific receipt validation result
565
624
  */
566
625
  export const validateReceipt = async (sku, androidOptions) => {
567
- if (!iap) {
568
- const errorJson = parseErrorStringToJsonObj('RnIap: Service not initialized. Call initConnection() first.');
569
- throw new Error(errorJson.message);
570
- }
571
626
  try {
572
627
  const params = {
573
628
  sku,
574
629
  androidOptions
575
630
  };
576
- const nitroResult = await iap.validateReceipt(params);
631
+ const nitroResult = await getIap().validateReceipt(params);
577
632
 
578
633
  // Convert Nitro result to public API result
579
634
  if (Platform.OS === 'ios') {
@@ -626,12 +681,8 @@ export const syncIOS = async () => {
626
681
  if (Platform.OS !== 'ios') {
627
682
  throw new Error('syncIOS is only available on iOS');
628
683
  }
629
- if (!iap) {
630
- const errorJson = parseErrorStringToJsonObj('RnIap: Service not initialized. Call initConnection() first.');
631
- throw new Error(errorJson.message);
632
- }
633
684
  try {
634
- return await iap.syncIOS();
685
+ return await getIap().syncIOS();
635
686
  } catch (error) {
636
687
  console.error('[syncIOS] Failed:', error);
637
688
  const errorJson = parseErrorStringToJsonObj(error);
@@ -648,12 +699,8 @@ export const requestPromotedProductIOS = async () => {
648
699
  if (Platform.OS !== 'ios') {
649
700
  return null;
650
701
  }
651
- if (!iap) {
652
- const errorJson = parseErrorStringToJsonObj('RnIap: Service not initialized. Call initConnection() first.');
653
- throw new Error(errorJson.message);
654
- }
655
702
  try {
656
- const nitroProduct = await iap.requestPromotedProductIOS();
703
+ const nitroProduct = await getIap().requestPromotedProductIOS();
657
704
  if (nitroProduct) {
658
705
  return convertNitroProductToProduct(nitroProduct);
659
706
  }
@@ -674,12 +721,8 @@ export const presentCodeRedemptionSheetIOS = async () => {
674
721
  if (Platform.OS !== 'ios') {
675
722
  return false;
676
723
  }
677
- if (!iap) {
678
- const errorJson = parseErrorStringToJsonObj('RnIap: Service not initialized. Call initConnection() first.');
679
- throw new Error(errorJson.message);
680
- }
681
724
  try {
682
- return await iap.presentCodeRedemptionSheetIOS();
725
+ return await getIap().presentCodeRedemptionSheetIOS();
683
726
  } catch (error) {
684
727
  console.error('[presentCodeRedemptionSheetIOS] Failed:', error);
685
728
  const errorJson = parseErrorStringToJsonObj(error);
@@ -696,12 +739,8 @@ export const buyPromotedProductIOS = async () => {
696
739
  if (Platform.OS !== 'ios') {
697
740
  throw new Error('buyPromotedProductIOS is only available on iOS');
698
741
  }
699
- if (!iap) {
700
- const errorJson = parseErrorStringToJsonObj('RnIap: Service not initialized. Call initConnection() first.');
701
- throw new Error(errorJson.message);
702
- }
703
742
  try {
704
- await iap.buyPromotedProductIOS();
743
+ await getIap().buyPromotedProductIOS();
705
744
  } catch (error) {
706
745
  console.error('[buyPromotedProductIOS] Failed:', error);
707
746
  const errorJson = parseErrorStringToJsonObj(error);
@@ -718,12 +757,8 @@ export const clearTransactionIOS = async () => {
718
757
  if (Platform.OS !== 'ios') {
719
758
  return;
720
759
  }
721
- if (!iap) {
722
- const errorJson = parseErrorStringToJsonObj('RnIap: Service not initialized. Call initConnection() first.');
723
- throw new Error(errorJson.message);
724
- }
725
760
  try {
726
- await iap.clearTransactionIOS();
761
+ await getIap().clearTransactionIOS();
727
762
  } catch (error) {
728
763
  console.error('[clearTransactionIOS] Failed:', error);
729
764
  const errorJson = parseErrorStringToJsonObj(error);
@@ -741,12 +776,8 @@ export const beginRefundRequestIOS = async sku => {
741
776
  if (Platform.OS !== 'ios') {
742
777
  return null;
743
778
  }
744
- if (!iap) {
745
- const errorJson = parseErrorStringToJsonObj('RnIap: Service not initialized. Call initConnection() first.');
746
- throw new Error(errorJson.message);
747
- }
748
779
  try {
749
- return await iap.beginRefundRequestIOS(sku);
780
+ return await getIap().beginRefundRequestIOS(sku);
750
781
  } catch (error) {
751
782
  console.error('[beginRefundRequestIOS] Failed:', error);
752
783
  const errorJson = parseErrorStringToJsonObj(error);
@@ -765,12 +796,8 @@ export const subscriptionStatusIOS = async sku => {
765
796
  if (Platform.OS !== 'ios') {
766
797
  throw new Error('subscriptionStatusIOS is only available on iOS');
767
798
  }
768
- if (!iap) {
769
- const errorJson = parseErrorStringToJsonObj('RnIap: Service not initialized. Call initConnection() first.');
770
- throw new Error(errorJson.message);
771
- }
772
799
  try {
773
- const statuses = await iap.subscriptionStatusIOS(sku);
800
+ const statuses = await getIap().subscriptionStatusIOS(sku);
774
801
  if (!statuses || !Array.isArray(statuses)) return [];
775
802
  return statuses.map(s => convertNitroSubscriptionStatusToSubscriptionStatusIOS(s));
776
803
  } catch (error) {
@@ -790,12 +817,8 @@ export const currentEntitlementIOS = async sku => {
790
817
  if (Platform.OS !== 'ios') {
791
818
  return null;
792
819
  }
793
- if (!iap) {
794
- const errorJson = parseErrorStringToJsonObj('RnIap: Service not initialized. Call initConnection() first.');
795
- throw new Error(errorJson.message);
796
- }
797
820
  try {
798
- const nitroPurchase = await iap.currentEntitlementIOS(sku);
821
+ const nitroPurchase = await getIap().currentEntitlementIOS(sku);
799
822
  if (nitroPurchase) {
800
823
  return convertNitroPurchaseToPurchase(nitroPurchase);
801
824
  }
@@ -817,12 +840,8 @@ export const latestTransactionIOS = async sku => {
817
840
  if (Platform.OS !== 'ios') {
818
841
  return null;
819
842
  }
820
- if (!iap) {
821
- const errorJson = parseErrorStringToJsonObj('RnIap: Service not initialized. Call initConnection() first.');
822
- throw new Error(errorJson.message);
823
- }
824
843
  try {
825
- const nitroPurchase = await iap.latestTransactionIOS(sku);
844
+ const nitroPurchase = await getIap().latestTransactionIOS(sku);
826
845
  if (nitroPurchase) {
827
846
  return convertNitroPurchaseToPurchase(nitroPurchase);
828
847
  }
@@ -843,12 +862,8 @@ export const getPendingTransactionsIOS = async () => {
843
862
  if (Platform.OS !== 'ios') {
844
863
  return [];
845
864
  }
846
- if (!iap) {
847
- const errorJson = parseErrorStringToJsonObj('RnIap: Service not initialized. Call initConnection() first.');
848
- throw new Error(errorJson.message);
849
- }
850
865
  try {
851
- const nitroPurchases = await iap.getPendingTransactionsIOS();
866
+ const nitroPurchases = await getIap().getPendingTransactionsIOS();
852
867
  return nitroPurchases.map(convertNitroPurchaseToPurchase);
853
868
  } catch (error) {
854
869
  console.error('[getPendingTransactionsIOS] Failed:', error);
@@ -866,12 +881,8 @@ export const showManageSubscriptionsIOS = async () => {
866
881
  if (Platform.OS !== 'ios') {
867
882
  return [];
868
883
  }
869
- if (!iap) {
870
- const errorJson = parseErrorStringToJsonObj('RnIap: Service not initialized. Call initConnection() first.');
871
- throw new Error(errorJson.message);
872
- }
873
884
  try {
874
- const nitroPurchases = await iap.showManageSubscriptionsIOS();
885
+ const nitroPurchases = await getIap().showManageSubscriptionsIOS();
875
886
  return nitroPurchases.map(convertNitroPurchaseToPurchase);
876
887
  } catch (error) {
877
888
  console.error('[showManageSubscriptionsIOS] Failed:', error);
@@ -890,12 +901,8 @@ export const isEligibleForIntroOfferIOS = async groupID => {
890
901
  if (Platform.OS !== 'ios') {
891
902
  return false;
892
903
  }
893
- if (!iap) {
894
- const errorJson = parseErrorStringToJsonObj('RnIap: Service not initialized. Call initConnection() first.');
895
- throw new Error(errorJson.message);
896
- }
897
904
  try {
898
- return await iap.isEligibleForIntroOfferIOS(groupID);
905
+ return await getIap().isEligibleForIntroOfferIOS(groupID);
899
906
  } catch (error) {
900
907
  console.error('[isEligibleForIntroOfferIOS] Failed:', error);
901
908
  const errorJson = parseErrorStringToJsonObj(error);
@@ -912,12 +919,8 @@ export const getReceiptDataIOS = async () => {
912
919
  if (Platform.OS !== 'ios') {
913
920
  throw new Error('getReceiptDataIOS is only available on iOS');
914
921
  }
915
- if (!iap) {
916
- const errorJson = parseErrorStringToJsonObj('RnIap: Service not initialized. Call initConnection() first.');
917
- throw new Error(errorJson.message);
918
- }
919
922
  try {
920
- return await iap.getReceiptDataIOS();
923
+ return await getIap().getReceiptDataIOS();
921
924
  } catch (error) {
922
925
  console.error('[getReceiptDataIOS] Failed:', error);
923
926
  const errorJson = parseErrorStringToJsonObj(error);
@@ -935,12 +938,8 @@ export const isTransactionVerifiedIOS = async sku => {
935
938
  if (Platform.OS !== 'ios') {
936
939
  return false;
937
940
  }
938
- if (!iap) {
939
- const errorJson = parseErrorStringToJsonObj('RnIap: Service not initialized. Call initConnection() first.');
940
- throw new Error(errorJson.message);
941
- }
942
941
  try {
943
- return await iap.isTransactionVerifiedIOS(sku);
942
+ return await getIap().isTransactionVerifiedIOS(sku);
944
943
  } catch (error) {
945
944
  console.error('[isTransactionVerifiedIOS] Failed:', error);
946
945
  const errorJson = parseErrorStringToJsonObj(error);
@@ -958,12 +957,8 @@ export const getTransactionJwsIOS = async sku => {
958
957
  if (Platform.OS !== 'ios') {
959
958
  return null;
960
959
  }
961
- if (!iap) {
962
- const errorJson = parseErrorStringToJsonObj('RnIap: Service not initialized. Call initConnection() first.');
963
- throw new Error(errorJson.message);
964
- }
965
960
  try {
966
- return await iap.getTransactionJwsIOS(sku);
961
+ return await getIap().getTransactionJwsIOS(sku);
967
962
  } catch (error) {
968
963
  console.error('[getTransactionJwsIOS] Failed:', error);
969
964
  const errorJson = parseErrorStringToJsonObj(error);
@@ -988,7 +983,7 @@ export const getStorefrontIOS = async () => {
988
983
  }
989
984
  try {
990
985
  // Call the native method to get storefront
991
- const storefront = await iap.getStorefrontIOS();
986
+ const storefront = await getIap().getStorefrontIOS();
992
987
  return storefront;
993
988
  } catch (error) {
994
989
  console.error('Failed to get storefront:', error);
@@ -1006,7 +1001,7 @@ export const getStorefront = async () => {
1006
1001
  if (Platform.OS === 'android') {
1007
1002
  try {
1008
1003
  // Optional since older builds may not have the method
1009
- const result = await iap.getStorefrontAndroid?.();
1004
+ const result = await getIap().getStorefrontAndroid?.();
1010
1005
  return result ?? '';
1011
1006
  } catch {
1012
1007
  return '';
@@ -1021,7 +1016,7 @@ export const getStorefront = async () => {
1021
1016
  */
1022
1017
  export const deepLinkToSubscriptions = async (options = {}) => {
1023
1018
  if (Platform.OS === 'android') {
1024
- await iap.deepLinkToSubscriptionsAndroid?.({
1019
+ await getIap().deepLinkToSubscriptionsAndroid?.({
1025
1020
  skuAndroid: options.skuAndroid,
1026
1021
  packageNameAndroid: options.packageNameAndroid
1027
1022
  });
@@ -1030,7 +1025,7 @@ export const deepLinkToSubscriptions = async (options = {}) => {
1030
1025
  // iOS: Use manage subscriptions sheet (ignore returned purchases for deeplink parity)
1031
1026
  if (Platform.OS === 'ios') {
1032
1027
  try {
1033
- await iap.showManageSubscriptionsIOS();
1028
+ await getIap().showManageSubscriptionsIOS();
1034
1029
  } catch {
1035
1030
  // no-op
1036
1031
  }
@@ -1064,7 +1059,7 @@ export const getAppTransactionIOS = async () => {
1064
1059
  }
1065
1060
  try {
1066
1061
  // Call the native method to get app transaction
1067
- const appTransaction = await iap.getAppTransactionIOS();
1062
+ const appTransaction = await getIap().getAppTransactionIOS();
1068
1063
  return appTransaction;
1069
1064
  } catch (error) {
1070
1065
  console.error('Failed to get app transaction:', error);