expo-iap 4.3.3 → 4.3.4

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.
@@ -3,6 +3,7 @@ package expo.modules.iap
3
3
  import android.content.Context
4
4
  import dev.hyo.openiap.AndroidSubscriptionOfferInput
5
5
  import dev.hyo.openiap.DeepLinkOptions
6
+ import dev.hyo.openiap.FetchProductsResultAll
6
7
  import dev.hyo.openiap.FetchProductsResultProducts
7
8
  import dev.hyo.openiap.FetchProductsResultSubscriptions
8
9
  import dev.hyo.openiap.InitConnectionConfig
@@ -183,9 +184,9 @@ class ExpoIapModule : Module() {
183
184
  val result = openIap.fetchProducts(request)
184
185
  val payload =
185
186
  when (result) {
187
+ is FetchProductsResultAll -> result.value.orEmpty().map { it.toJson() }
186
188
  is FetchProductsResultProducts -> result.value.orEmpty().map { it.toJson() }
187
189
  is FetchProductsResultSubscriptions -> result.value.orEmpty().map { it.toJson() }
188
- else -> emptyList<Map<String, Any?>>()
189
190
  }
190
191
  ExpoIapLog.result("fetchProducts", payload)
191
192
  promise.resolve(payload)
@@ -50,6 +50,7 @@ public final class ExpoIapOnsideModule: Module {
50
50
  private let transactionObserver = OnsideTransactionObserverBridge()
51
51
  private let productFetcher = OnsideProductFetcher()
52
52
  private var productCache: [String: OnsideProduct] = [:]
53
+ private var transactionDateCache: [String: Date] = [:]
53
54
 
54
55
  nonisolated public func definition() -> ModuleDefinition {
55
56
  Name("ExpoIapOnside")
@@ -113,7 +114,7 @@ public final class ExpoIapOnsideModule: Module {
113
114
 
114
115
  // Check if Onside Store is installed
115
116
  if let onsideURL = URL(string: "onside://"),
116
- UIApplication.shared.canOpenURL(onsideURL) {
117
+ await MainActor.run(body: { UIApplication.shared.canOpenURL(onsideURL) }) {
117
118
  #if DEBUG
118
119
  print("[ExpoIapOnsideModule] ✅ Onside Store app is installed")
119
120
  #endif
@@ -225,16 +226,15 @@ public final class ExpoIapOnsideModule: Module {
225
226
  let productId = purchasePayload["productId"] as? String
226
227
  let txId = purchasePayload["transactionId"] as? String
227
228
 
228
- let queue = await Onside.defaultPaymentQueue()
229
-
230
229
  let transaction: OnsidePaymentTransaction? = await MainActor.run {
230
+ let transactions = Onside.defaultPaymentQueue().transactions
231
231
  if let txId, !txId.isEmpty {
232
- return queue.transactions.first(where: { $0.transactionIdentifier == txId })
232
+ return transactions.first(where: { $0.transactionIdentifier == txId })
233
233
  }
234
234
 
235
235
  // 2) fallback: if txId is not available yet — search by productId (less reliable!)
236
236
  if let productId, !productId.isEmpty {
237
- return queue.transactions.first(where: {
237
+ return transactions.first(where: {
238
238
  $0.payment.product.productIdentifier == productId
239
239
  && ($0.transactionState == .purchased || $0.transactionState == .restored)
240
240
  })
@@ -247,7 +247,9 @@ public final class ExpoIapOnsideModule: Module {
247
247
  throw OnsideBridgeError.transactionNotFound(txId ?? productId ?? "")
248
248
  }
249
249
 
250
- await queue.finishTransaction(transaction)
250
+ await MainActor.run {
251
+ Onside.defaultPaymentQueue().finishTransaction(transaction)
252
+ }
251
253
  ExpoIapLog.result("finishTransactionOnside", value: true)
252
254
  return true
253
255
  }
@@ -298,14 +300,21 @@ public final class ExpoIapOnsideModule: Module {
298
300
  ]
299
301
  )
300
302
  try await ensureObserverRegistered()
301
- let queue = await Onside.defaultPaymentQueue()
302
- let payload = try queue.transactions.compactMap { transaction -> [String: Any]? in
303
- switch transaction.transactionState {
304
- case .purchased, .restored:
305
- return try serialize(transaction: transaction)
306
- default:
307
- return nil
303
+ let payload: [[String: Any]] = try await MainActor.run {
304
+ let items = try Onside.defaultPaymentQueue().transactions.compactMap { transaction -> [String: Any]? in
305
+ switch transaction.transactionState {
306
+ case .purchased, .restored:
307
+ return try serialize(transaction: transaction)
308
+ default:
309
+ return nil
310
+ }
308
311
  }
312
+ if alsoPublish {
313
+ items.forEach {
314
+ sendEvent(OnsideEvent.purchaseUpdated.rawValue, $0)
315
+ }
316
+ }
317
+ return items
309
318
  }
310
319
  ExpoIapLog.result("getAvailableItemsOnside", value: payload)
311
320
  return payload
@@ -323,7 +332,7 @@ public final class ExpoIapOnsideModule: Module {
323
332
  private func getOnsideStorefront() async throws -> String {
324
333
  ExpoIapLog.payload("getStorefrontOnside", payload: nil)
325
334
  try await ensureObserverRegistered()
326
- let storefront = await Onside.defaultPaymentQueue().storefront?.countryCode ?? ""
335
+ let storefront = Onside.defaultPaymentQueue().storefront?.countryCode ?? ""
327
336
  ExpoIapLog.result("getStorefrontOnside", value: storefront)
328
337
  return storefront
329
338
  }
@@ -385,6 +394,7 @@ public final class ExpoIapOnsideModule: Module {
385
394
  let cont = restoreContinuation
386
395
  restoreContinuation = nil
387
396
  cont?.resume(returning: false)
397
+ transactionDateCache.removeAll()
388
398
  }
389
399
 
390
400
  private func handle(transaction: OnsidePaymentTransaction) {
@@ -425,11 +435,11 @@ public final class ExpoIapOnsideModule: Module {
425
435
  dictionary["displayNameIOS"] = product.localizedTitle
426
436
  let formatter = NumberFormatter()
427
437
  formatter.numberStyle = .currency
428
- formatter.currencyCode = product.price.currencyCode ?? ""
429
- let priceNumber = NSDecimalNumber(decimal: product.price.value)
438
+ formatter.currencyCode = product.price.currencyCode
439
+ let priceNumber = NSDecimalNumber(string: String(product.price.value))
430
440
  let formattedPrice = formatter.string(from: priceNumber) ?? "\(product.price.value)"
431
441
  dictionary["displayPrice"] = formattedPrice
432
- dictionary["currency"] = product.price.currencyCode ?? ""
442
+ dictionary["currency"] = product.price.currencyCode
433
443
  dictionary["price"] = priceNumber
434
444
  dictionary["type"] = "in-app"
435
445
  dictionary["typeIOS"] = "non-consumable"
@@ -450,15 +460,15 @@ public final class ExpoIapOnsideModule: Module {
450
460
  dictionary["quantity"] = 1
451
461
  dictionary["isAutoRenewing"] = false
452
462
  dictionary["purchaseState"] = mapPurchaseState(transaction.transactionState)
453
- let txDate = transaction.transactionDate ?? Date()
463
+ let txDate = date(for: transaction)
454
464
  dictionary["transactionDate"] = Int(txDate.timeIntervalSince1970 * 1000)
455
- dictionary["currencyCodeIOS"] = product.price.currencyCode ?? ""
465
+ dictionary["currencyCodeIOS"] = product.price.currencyCode
456
466
  let currencyFormatter = NumberFormatter()
457
467
  currencyFormatter.numberStyle = .currency
458
- currencyFormatter.currencyCode = product.price.currencyCode ?? ""
468
+ currencyFormatter.currencyCode = product.price.currencyCode
459
469
  dictionary["currencySymbolIOS"] = currencyFormatter.currencySymbol ?? ""
460
470
 
461
- dictionary["storefrontCountryCodeIOS"] = transaction.storefront.countryCode ?? ""
471
+ dictionary["storefrontCountryCodeIOS"] = transaction.storefront.countryCode
462
472
  dictionary["purchaseToken"] = nil
463
473
  dictionary["environmentIOS"] = transaction.storefront.id
464
474
  if let error = transaction.error {
@@ -467,12 +477,30 @@ public final class ExpoIapOnsideModule: Module {
467
477
  return sanitize(dictionary)
468
478
  }
469
479
 
480
+ private func date(for transaction: OnsidePaymentTransaction) -> Date {
481
+ guard let key = transaction.transactionIdentifier ?? transaction.originalTransactionIdentifier,
482
+ !key.isEmpty
483
+ else {
484
+ return Date()
485
+ }
486
+
487
+ if let cachedDate = transactionDateCache[key] {
488
+ return cachedDate
489
+ }
490
+
491
+ // OnsideKit currently exposes no purchase date on the transaction, so
492
+ // keep the first observed timestamp stable for repeated serializations.
493
+ let observedDate = Date()
494
+ transactionDateCache[key] = observedDate
495
+ return observedDate
496
+ }
497
+
470
498
  // Build a JSON string from known product fields (no Encodable conformance required)
471
499
  private func makeProductJSONRepresentation(from product: OnsideProduct) throws -> String {
472
500
  let priceFormatter = NumberFormatter()
473
501
  priceFormatter.numberStyle = .currency
474
- priceFormatter.currencyCode = product.price.currencyCode ?? ""
475
- let priceNumber = NSDecimalNumber(decimal: product.price.value)
502
+ priceFormatter.currencyCode = product.price.currencyCode
503
+ let priceNumber = NSDecimalNumber(string: String(product.price.value))
476
504
  let formattedPrice = priceFormatter.string(from: priceNumber) ?? "\(product.price.value)"
477
505
  let jsonObject: [String: Any] = [
478
506
  "id": product.productIdentifier,
@@ -480,7 +508,7 @@ public final class ExpoIapOnsideModule: Module {
480
508
  "description": product.localizedDescription,
481
509
  "price": [
482
510
  "value": priceNumber,
483
- "currencyCode": product.price.currencyCode ?? "",
511
+ "currencyCode": product.price.currencyCode,
484
512
  "formatted": formattedPrice,
485
513
  ],
486
514
  "isFamilyShareable": false,
@@ -655,6 +683,7 @@ private final class OnsideProductFetcher: NSObject, OnsideProductsRequestDelegat
655
683
  }
656
684
  }
657
685
 
686
+ @MainActor
658
687
  private func cleanup() {
659
688
  request?.delegate = nil
660
689
  request?.stop()
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "spec": "2.0.2",
3
- "google": "2.2.2",
3
+ "google": "2.2.3",
4
4
  "apple": "2.2.1"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-iap",
3
- "version": "4.3.3",
3
+ "version": "4.3.4",
4
4
  "description": "In App Purchase module in Expo",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",