react-native-iap 14.2.0 → 14.2.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.
- package/NitroIap.podspec +1 -1
- package/ios/HybridRnIap.swift +58 -18
- package/package.json +1 -1
package/NitroIap.podspec
CHANGED
package/ios/HybridRnIap.swift
CHANGED
|
@@ -21,6 +21,10 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
21
21
|
private var purchaseUpdatedListeners: [(NitroPurchase) -> Void] = []
|
|
22
22
|
private var purchaseErrorListeners: [(NitroPurchaseResult) -> Void] = []
|
|
23
23
|
private var promotedProductListeners: [(NitroProduct) -> Void] = []
|
|
24
|
+
// Deduplication for purchase error events
|
|
25
|
+
private var lastPurchaseErrorCode: String? = nil
|
|
26
|
+
private var lastPurchaseErrorProductId: String? = nil
|
|
27
|
+
private var lastPurchaseErrorTimestamp: TimeInterval = 0
|
|
24
28
|
|
|
25
29
|
// MARK: - Initialization
|
|
26
30
|
|
|
@@ -53,6 +57,9 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
53
57
|
self.purchaseErrorSub = OpenIapModule.shared.purchaseErrorListener { [weak self] error in
|
|
54
58
|
guard let self else { return }
|
|
55
59
|
Task { @MainActor in
|
|
60
|
+
#if DEBUG
|
|
61
|
+
print("[HybridRnIap] purchaseError event: code=\(error.code), productId=\(error.productId ?? "-")")
|
|
62
|
+
#endif
|
|
56
63
|
let nitroError = self.createPurchaseErrorResult(
|
|
57
64
|
code: error.code,
|
|
58
65
|
message: error.message,
|
|
@@ -108,6 +115,9 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
108
115
|
self.purchaseErrorSub = OpenIapModule.shared.purchaseErrorListener { [weak self] error in
|
|
109
116
|
guard let self else { return }
|
|
110
117
|
Task { @MainActor in
|
|
118
|
+
#if DEBUG
|
|
119
|
+
print("[HybridRnIap] purchaseError event: code=\(error.code), productId=\(error.productId ?? "-")")
|
|
120
|
+
#endif
|
|
111
121
|
let nitroError = self.createPurchaseErrorResult(
|
|
112
122
|
code: error.code,
|
|
113
123
|
message: error.message,
|
|
@@ -182,7 +192,8 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
182
192
|
let products = try await OpenIapModule.shared.fetchProducts(req)
|
|
183
193
|
return products.map { self.convertOpenIapProductToNitroProduct($0) }
|
|
184
194
|
} catch {
|
|
185
|
-
|
|
195
|
+
// Propagate OpenIAP error
|
|
196
|
+
throw error
|
|
186
197
|
}
|
|
187
198
|
}
|
|
188
199
|
}
|
|
@@ -198,7 +209,19 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
198
209
|
return
|
|
199
210
|
}
|
|
200
211
|
do {
|
|
201
|
-
|
|
212
|
+
// Event-first behavior: don't reject Promise on connection issues
|
|
213
|
+
guard self.isInitialized else {
|
|
214
|
+
#if DEBUG
|
|
215
|
+
print("[HybridRnIap] requestPurchase while not initialized; sending E_INIT_CONNECTION")
|
|
216
|
+
#endif
|
|
217
|
+
let err = self.createPurchaseErrorResult(
|
|
218
|
+
code: OpenIapError.E_INIT_CONNECTION,
|
|
219
|
+
message: "IAP store connection not initialized",
|
|
220
|
+
productId: iosRequest.sku
|
|
221
|
+
)
|
|
222
|
+
self.sendPurchaseError(err)
|
|
223
|
+
return
|
|
224
|
+
}
|
|
202
225
|
// Delegate purchase to OpenIAP. It emits success/error events which we bridge above.
|
|
203
226
|
let props = OpenIapRequestPurchaseProps(
|
|
204
227
|
sku: iosRequest.sku,
|
|
@@ -216,13 +239,14 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
216
239
|
)
|
|
217
240
|
_ = try await OpenIapModule.shared.requestPurchase(props)
|
|
218
241
|
} catch {
|
|
219
|
-
//
|
|
242
|
+
// Ensure an error reaches JS even if OpenIAP threw before emitting.
|
|
243
|
+
// Use simple de-duplication window to avoid double-emitting.
|
|
220
244
|
let err = self.createPurchaseErrorResult(
|
|
221
245
|
code: OpenIapError.E_SERVICE_ERROR,
|
|
222
246
|
message: error.localizedDescription,
|
|
223
247
|
productId: iosRequest.sku
|
|
224
248
|
)
|
|
225
|
-
self.
|
|
249
|
+
self.sendPurchaseErrorDedup(err)
|
|
226
250
|
}
|
|
227
251
|
}
|
|
228
252
|
}
|
|
@@ -236,7 +260,8 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
236
260
|
let purchases = try await OpenIapModule.shared.getAvailablePurchases(props)
|
|
237
261
|
return purchases.map { self.convertOpenIapPurchaseToNitroPurchase($0) }
|
|
238
262
|
} catch {
|
|
239
|
-
|
|
263
|
+
// Propagate OpenIAP error or map to network error
|
|
264
|
+
throw OpenIapError.make(code: OpenIapError.E_NETWORK_ERROR)
|
|
240
265
|
}
|
|
241
266
|
}
|
|
242
267
|
}
|
|
@@ -250,7 +275,7 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
250
275
|
return .first(ok)
|
|
251
276
|
} catch {
|
|
252
277
|
let tid = iosParams.transactionId
|
|
253
|
-
throw
|
|
278
|
+
throw OpenIapError.make(code: OpenIapError.E_PURCHASE_ERROR, message: "Transaction not found: \(tid)")
|
|
254
279
|
}
|
|
255
280
|
}
|
|
256
281
|
}
|
|
@@ -271,7 +296,7 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
271
296
|
)
|
|
272
297
|
return .first(mapped)
|
|
273
298
|
} catch {
|
|
274
|
-
throw
|
|
299
|
+
throw OpenIapError.make(code: OpenIapError.E_RECEIPT_FAILED, message: error.localizedDescription)
|
|
275
300
|
}
|
|
276
301
|
}
|
|
277
302
|
}
|
|
@@ -283,7 +308,7 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
283
308
|
do {
|
|
284
309
|
return try await OpenIapModule.shared.getStorefrontIOS()
|
|
285
310
|
} catch {
|
|
286
|
-
throw
|
|
311
|
+
throw OpenIapError.make(code: OpenIapError.E_SERVICE_ERROR, message: error.localizedDescription)
|
|
287
312
|
}
|
|
288
313
|
}
|
|
289
314
|
}
|
|
@@ -347,7 +372,7 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
347
372
|
do {
|
|
348
373
|
try await OpenIapModule.shared.requestPurchaseOnPromotedProductIOS()
|
|
349
374
|
} catch {
|
|
350
|
-
|
|
375
|
+
// Event-only: OpenIAP will emit purchaseError for this flow. Avoid Promise rejection.
|
|
351
376
|
}
|
|
352
377
|
}
|
|
353
378
|
}
|
|
@@ -359,7 +384,7 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
359
384
|
return ok
|
|
360
385
|
} catch {
|
|
361
386
|
// Fallback with explicit error for simulator or unsupported cases
|
|
362
|
-
throw
|
|
387
|
+
throw OpenIapError.make(code: OpenIapError.E_FEATURE_NOT_SUPPORTED)
|
|
363
388
|
}
|
|
364
389
|
}
|
|
365
390
|
}
|
|
@@ -423,7 +448,7 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
423
448
|
}
|
|
424
449
|
return nil
|
|
425
450
|
} catch {
|
|
426
|
-
throw
|
|
451
|
+
throw OpenIapError.make(code: OpenIapError.E_SKU_NOT_FOUND, productId: sku)
|
|
427
452
|
}
|
|
428
453
|
}
|
|
429
454
|
}
|
|
@@ -437,7 +462,7 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
437
462
|
}
|
|
438
463
|
return nil
|
|
439
464
|
} catch {
|
|
440
|
-
throw
|
|
465
|
+
throw OpenIapError.make(code: OpenIapError.E_SKU_NOT_FOUND, productId: sku)
|
|
441
466
|
}
|
|
442
467
|
}
|
|
443
468
|
}
|
|
@@ -459,7 +484,7 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
459
484
|
let ok = try await OpenIapModule.shared.syncIOS()
|
|
460
485
|
return ok
|
|
461
486
|
} catch {
|
|
462
|
-
throw
|
|
487
|
+
throw OpenIapError.make(code: OpenIapError.E_SERVICE_ERROR, message: error.localizedDescription)
|
|
463
488
|
}
|
|
464
489
|
}
|
|
465
490
|
}
|
|
@@ -473,7 +498,7 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
473
498
|
let purchases = try await OpenIapModule.shared.getAvailablePurchases(OpenIapPurchaseOptions(alsoPublishToEventListenerIOS: false, onlyIncludeActiveItemsIOS: true))
|
|
474
499
|
return purchases.map { self.convertOpenIapPurchaseToNitroPurchase($0) }
|
|
475
500
|
} catch {
|
|
476
|
-
throw
|
|
501
|
+
throw OpenIapError.make(code: OpenIapError.E_SERVICE_ERROR, message: error.localizedDescription)
|
|
477
502
|
}
|
|
478
503
|
}
|
|
479
504
|
}
|
|
@@ -490,9 +515,9 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
490
515
|
if let receipt = try await OpenIapModule.shared.getReceiptDataIOS() {
|
|
491
516
|
return receipt
|
|
492
517
|
}
|
|
493
|
-
throw
|
|
518
|
+
throw OpenIapError.make(code: OpenIapError.E_RECEIPT_FAILED)
|
|
494
519
|
} catch {
|
|
495
|
-
throw
|
|
520
|
+
throw OpenIapError.make(code: OpenIapError.E_RECEIPT_FAILED, message: error.localizedDescription)
|
|
496
521
|
}
|
|
497
522
|
}
|
|
498
523
|
}
|
|
@@ -508,7 +533,7 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
508
533
|
return Promise.async {
|
|
509
534
|
try self.ensureConnection()
|
|
510
535
|
do { return try await OpenIapModule.shared.getTransactionJwsIOS(sku: sku) } catch {
|
|
511
|
-
throw
|
|
536
|
+
throw OpenIapError.make(code: OpenIapError.E_TRANSACTION_VALIDATION_FAILED, message: "Can't find transaction for sku \(sku)")
|
|
512
537
|
}
|
|
513
538
|
}
|
|
514
539
|
}
|
|
@@ -577,7 +602,7 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
577
602
|
|
|
578
603
|
private func ensureConnection() throws {
|
|
579
604
|
guard isInitialized else {
|
|
580
|
-
throw
|
|
605
|
+
throw OpenIapError.make(code: OpenIapError.E_INIT_CONNECTION, message: "Connection not initialized. Call initConnection() first.")
|
|
581
606
|
}
|
|
582
607
|
}
|
|
583
608
|
|
|
@@ -588,10 +613,25 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
588
613
|
}
|
|
589
614
|
|
|
590
615
|
private func sendPurchaseError(_ error: NitroPurchaseResult) {
|
|
616
|
+
// Update last error for deduplication
|
|
617
|
+
lastPurchaseErrorCode = error.code
|
|
618
|
+
lastPurchaseErrorProductId = error.purchaseToken
|
|
619
|
+
lastPurchaseErrorTimestamp = Date().timeIntervalSince1970
|
|
591
620
|
for listener in purchaseErrorListeners {
|
|
592
621
|
listener(error)
|
|
593
622
|
}
|
|
594
623
|
}
|
|
624
|
+
|
|
625
|
+
private func sendPurchaseErrorDedup(_ error: NitroPurchaseResult) {
|
|
626
|
+
let now = Date().timeIntervalSince1970
|
|
627
|
+
let sameCode = (error.code == lastPurchaseErrorCode)
|
|
628
|
+
let sameProduct = (error.purchaseToken == lastPurchaseErrorProductId)
|
|
629
|
+
let withinWindow = (now - lastPurchaseErrorTimestamp) < 0.3
|
|
630
|
+
if sameCode && sameProduct && withinWindow {
|
|
631
|
+
return
|
|
632
|
+
}
|
|
633
|
+
sendPurchaseError(error)
|
|
634
|
+
}
|
|
595
635
|
|
|
596
636
|
private func createPurchaseErrorResult(code: String, message: String, productId: String? = nil) -> NitroPurchaseResult {
|
|
597
637
|
var result = NitroPurchaseResult()
|
package/package.json
CHANGED