expo-iap 1.0.3 → 2.0.0-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.
Files changed (103) hide show
  1. package/.eslintrc.js +9 -0
  2. package/.prettierrc.js +9 -0
  3. package/.swiftlint.yml +10 -0
  4. package/README.md +28 -21
  5. package/android/build.gradle +34 -72
  6. package/android/src/main/AndroidManifest.xml +1 -4
  7. package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +535 -0
  8. package/android/src/main/java/expo/modules/iap/MissingCurrentActivityException.kt +6 -0
  9. package/android/src/main/java/expo/modules/iap/PlayUtils.kt +124 -0
  10. package/build/ExpoIap.types.d.ts +89 -0
  11. package/build/ExpoIap.types.d.ts.map +1 -0
  12. package/build/ExpoIap.types.js +59 -0
  13. package/build/ExpoIap.types.js.map +1 -0
  14. package/build/ExpoIapModule.d.ts +3 -0
  15. package/build/ExpoIapModule.d.ts.map +1 -0
  16. package/build/ExpoIapModule.js +5 -0
  17. package/build/ExpoIapModule.js.map +1 -0
  18. package/build/index.d.ts +38 -0
  19. package/build/index.d.ts.map +1 -0
  20. package/build/index.js +202 -0
  21. package/build/index.js.map +1 -0
  22. package/build/modules/android.d.ts +40 -0
  23. package/build/modules/android.d.ts.map +1 -0
  24. package/build/modules/android.js +54 -0
  25. package/build/modules/android.js.map +1 -0
  26. package/build/modules/ios.d.ts +41 -0
  27. package/build/modules/ios.d.ts.map +1 -0
  28. package/build/modules/ios.js +44 -0
  29. package/build/modules/ios.js.map +1 -0
  30. package/build/types/ExpoIapAndroid.types.d.ts +113 -0
  31. package/build/types/ExpoIapAndroid.types.d.ts.map +1 -0
  32. package/build/types/ExpoIapAndroid.types.js +23 -0
  33. package/build/types/ExpoIapAndroid.types.js.map +1 -0
  34. package/build/types/ExpoIapIos.types.d.ts +122 -0
  35. package/build/types/ExpoIapIos.types.d.ts.map +1 -0
  36. package/build/types/ExpoIapIos.types.js +2 -0
  37. package/build/types/ExpoIapIos.types.js.map +1 -0
  38. package/bun.lockb +0 -0
  39. package/expo-module.config.json +4 -8
  40. package/ios/ExpoIap.podspec +27 -0
  41. package/ios/ExpoIapModule.swift +498 -0
  42. package/ios/ProductStore.swift +27 -0
  43. package/ios/Types.swift +54 -0
  44. package/package.json +33 -62
  45. package/src/ExpoIap.types.ts +125 -0
  46. package/src/ExpoIapModule.ts +5 -0
  47. package/src/index.ts +397 -0
  48. package/src/modules/android.ts +89 -0
  49. package/src/modules/ios.ts +81 -0
  50. package/src/types/ExpoIapAndroid.types.ts +123 -0
  51. package/src/types/ExpoIapIos.types.ts +141 -0
  52. package/tsconfig.json +9 -0
  53. package/.editorconfig +0 -10
  54. package/.flowconfig +0 -11
  55. package/.monolinterrc +0 -3
  56. package/.yarn/install-state.gz +0 -0
  57. package/.yarn/releases/yarn-3.1.1.cjs +0 -768
  58. package/.yarnrc.yml +0 -3
  59. package/LICENSE +0 -21
  60. package/RNIap.podspec +0 -18
  61. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  62. package/android/gradle/wrapper/gradle-wrapper.properties +0 -6
  63. package/android/gradle.properties +0 -2
  64. package/android/gradlew +0 -160
  65. package/android/gradlew.bat +0 -90
  66. package/android/libs/in-app-purchasing-2.0.76.jar +0 -0
  67. package/android/src/amazon/AndroidManifest.xml +0 -12
  68. package/android/src/amazon/java/com/dooboolab/RNIap/RNIapAmazonListener.kt +0 -356
  69. package/android/src/amazon/java/com/dooboolab/RNIap/RNIapAmazonModule.kt +0 -128
  70. package/android/src/amazon/java/com/dooboolab/RNIap/RNIapPackage.kt +0 -20
  71. package/android/src/main/java/com/dooboolab/RNIap/DoobooUtils.kt +0 -180
  72. package/android/src/play/java/com/dooboolab/RNIap/PlayUtils.kt +0 -77
  73. package/android/src/play/java/com/dooboolab/RNIap/RNIapModule.kt +0 -698
  74. package/android/src/play/java/com/dooboolab/RNIap/RNIapPackage.kt +0 -20
  75. package/babel.config.js +0 -10
  76. package/index.d.ts +0 -3
  77. package/index.js +0 -3
  78. package/index.js.flow +0 -9
  79. package/ios/RNIap.xcodeproj/project.pbxproj +0 -370
  80. package/ios/RNIap.xcodeproj/xcshareddata/xcschemes/RNIap.xcscheme +0 -80
  81. package/ios/RNIapIos.m +0 -60
  82. package/ios/RNIapIos.swift +0 -932
  83. package/ios/RNIapQueue.swift +0 -35
  84. package/jest.config.js +0 -194
  85. package/src/__test__/iap.test.d.ts +0 -1
  86. package/src/__test__/iap.test.js +0 -59
  87. package/src/hooks/useIAP.d.ts +0 -21
  88. package/src/hooks/useIAP.js +0 -140
  89. package/src/hooks/withIAPContext.d.ts +0 -21
  90. package/src/hooks/withIAPContext.js +0 -142
  91. package/src/iap.d.ts +0 -197
  92. package/src/iap.js +0 -625
  93. package/src/index.d.ts +0 -4
  94. package/src/index.js +0 -4
  95. package/src/types/amazon.d.ts +0 -23
  96. package/src/types/amazon.js +0 -1
  97. package/src/types/android.d.ts +0 -47
  98. package/src/types/android.js +0 -22
  99. package/src/types/apple.d.ts +0 -424
  100. package/src/types/apple.js +0 -165
  101. package/src/types/index.d.ts +0 -117
  102. package/src/types/index.js +0 -40
  103. package/test/mocks/react-native-modules.js +0 -14
@@ -0,0 +1,498 @@
1
+ import ExpoModulesCore
2
+ import StoreKit
3
+
4
+ func serializeDebug (_ s: String) -> String? {
5
+ #if DEBUG
6
+ return s
7
+ #else
8
+ return nil
9
+ #endif
10
+ }
11
+
12
+ struct IapEvent {
13
+ static let PurchaseUpdated = "purchase-updated"
14
+ static let PurchaseError = "purchase-error"
15
+ static let TransactionIapUpdated = "iap-transaction-updated"
16
+ }
17
+
18
+ @available(iOS 15.0, *)
19
+ func serializeProduct(_ p: Product) -> [String: Any?] {
20
+ return [
21
+ "debugDescription": serializeDebug(p.debugDescription),
22
+ "description": p.description,
23
+ "displayName": p.displayName,
24
+ "displayPrice": p.displayPrice,
25
+ "id": p.id,
26
+ "isFamilyShareable": p.isFamilyShareable,
27
+ "jsonRepresentation": serializeDebug(String(data: p.jsonRepresentation, encoding: .utf8) ?? ""),
28
+ "price": p.price,
29
+ "subscription": p.subscription,
30
+ "type": p.type,
31
+ "currency": p.priceFormatStyle.currencyCode
32
+ ]
33
+ }
34
+
35
+ @available(iOS 15.0, *)
36
+ func serializeTransaction(_ transaction: Transaction) -> [String: Any?] {
37
+ return [
38
+ "id": transaction.id,
39
+ "productID": transaction.productID,
40
+ "purchaseDate": transaction.purchaseDate,
41
+ "expirationDate": transaction.expirationDate,
42
+ "originalID": transaction.originalID
43
+ ]
44
+ }
45
+
46
+ @available(iOS 15.0, *)
47
+ func serializeSubscriptionStatus(_ status: Product.SubscriptionInfo.Status) -> [String: Any?] {
48
+ return [
49
+ "state": status.state.rawValue,
50
+ "renewalInfo": serializeRenewalInfo(status.renewalInfo)
51
+ ]
52
+ }
53
+
54
+ @available(iOS 15.0, *)
55
+ func serializeRenewalInfo(_ renewalInfo: VerificationResult<Product.SubscriptionInfo.RenewalInfo>) -> [String: Any?]? {
56
+ switch renewalInfo {
57
+ case .unverified:
58
+ return nil
59
+
60
+ case .verified(let info):
61
+ return [
62
+ "autoRenewStatus": info.willAutoRenew,
63
+ "autoRenewPreference": info.autoRenewPreference,
64
+ "expirationReason": info.expirationReason,
65
+ "deviceVerification": info.deviceVerification,
66
+ "currentProductID": info.currentProductID,
67
+ "debugDescription": info.debugDescription,
68
+ "gracePeriodExpirationDate": info.gracePeriodExpirationDate
69
+ ]
70
+ }
71
+ }
72
+
73
+ @available(iOS 15.0, *)
74
+ func serialize(_ transaction: Transaction, _ result: VerificationResult<Transaction>) -> [String: Any?] {
75
+ return serializeTransaction(transaction)
76
+ }
77
+
78
+ @available(iOS 15.0, *)
79
+ @Sendable func serialize(_ rs: Transaction.RefundRequestStatus?) -> String? {
80
+ guard let rs = rs else { return nil }
81
+ switch rs {
82
+ case .success: return "success"
83
+ case .userCancelled: return "userCancelled"
84
+ default:
85
+ return nil
86
+ }
87
+ }
88
+
89
+ @available(iOS 15.0, *)
90
+ public class ExpoIapModule: Module {
91
+ private var transactions: [String: Transaction] = [:]
92
+ private var productStore: ProductStore?
93
+ private var hasListeners = false
94
+ private var updateListenerTask: Task<Void, Error>?
95
+
96
+ public func definition() -> ModuleDefinition {
97
+ Name("ExpoIap")
98
+
99
+ Constants([
100
+ "PI": Double.pi
101
+ ])
102
+
103
+ Events(IapEvent.PurchaseUpdated, IapEvent.PurchaseError, IapEvent.TransactionIapUpdated)
104
+
105
+ OnStartObserving {
106
+ hasListeners = true
107
+ self.addTransactionObserver()
108
+ }
109
+
110
+ OnStopObserving {
111
+ hasListeners = false
112
+ self.removeTransactionObserver()
113
+ }
114
+
115
+ Function("initConnection") {
116
+ self.productStore = ProductStore()
117
+ return AppStore.canMakePayments
118
+ }
119
+
120
+ AsyncFunction("getItems") { (skus: [String]) -> [[String: Any?]?] in
121
+ guard let productStore = self.productStore else {
122
+ throw NSError(domain: "ExpoIapModule", code: 1, userInfo: [NSLocalizedDescriptionKey: "Connection not initialized"])
123
+ }
124
+
125
+ do {
126
+ let fetchedProducts = try await Product.products(for: skus)
127
+
128
+ await productStore.performOnActor { isolatedStore in
129
+ fetchedProducts.forEach({ product in
130
+ isolatedStore.addProduct(product)
131
+ })
132
+ }
133
+
134
+ let products = await productStore.getAllProducts()
135
+
136
+ return products.map { (prod: Product) -> [String: Any?]? in
137
+ return serializeProduct(prod)
138
+ }.compactMap { $0 }
139
+ } catch {
140
+ print("Error fetching items: \(error)")
141
+ throw error
142
+ }
143
+ }
144
+
145
+ AsyncFunction("endConnection") { () -> Bool in
146
+ guard let productStore = self.productStore else {
147
+ return false
148
+ }
149
+
150
+ await productStore.removeAll()
151
+ self.transactions.removeAll()
152
+ self.productStore = nil
153
+ self.removeTransactionObserver()
154
+ return true
155
+ }
156
+
157
+ AsyncFunction("getAvailableItems") { (alsoPublishToEventListener: Bool, onlyIncludeActiveItems: Bool) -> [[String: Any?]?] in
158
+ var purchasedItems: [Transaction] = []
159
+
160
+ func addTransaction(transaction: Transaction) {
161
+ purchasedItems.append(transaction)
162
+ if alsoPublishToEventListener {
163
+ self.sendEvent(IapEvent.PurchaseUpdated, serializeTransaction(transaction))
164
+ }
165
+ }
166
+
167
+ func addError(error: Error, errorDict: [String: String]) {
168
+ if alsoPublishToEventListener {
169
+ self.sendEvent(IapEvent.PurchaseError, errorDict)
170
+ }
171
+ }
172
+
173
+ for await result in onlyIncludeActiveItems ? Transaction.currentEntitlements : Transaction.all {
174
+ do {
175
+ let transaction = try self.checkVerified(result)
176
+ if !onlyIncludeActiveItems {
177
+ addTransaction(transaction: transaction)
178
+ continue
179
+ }
180
+ switch transaction.productType {
181
+ case .nonConsumable, .autoRenewable, .consumable:
182
+ if await self.productStore?.getProduct(productID: transaction.productID) != nil {
183
+ addTransaction(transaction: transaction)
184
+ }
185
+
186
+ case .nonRenewable:
187
+ if await self.productStore?.getProduct(productID: transaction.productID) != nil {
188
+ let currentDate = Date()
189
+ let expirationDate = Calendar(identifier: .gregorian).date(byAdding: DateComponents(year: 1), to: transaction.purchaseDate)!
190
+ if currentDate < expirationDate {
191
+ addTransaction(transaction: transaction)
192
+ }
193
+ }
194
+
195
+ default:
196
+ break
197
+ }
198
+ } catch StoreError.failedVerification {
199
+ let err = [
200
+ "responseCode": IapErrors.E_TRANSACTION_VALIDATION_FAILED.rawValue,
201
+ "debugMessage": StoreError.failedVerification.localizedDescription,
202
+ "code": IapErrors.E_TRANSACTION_VALIDATION_FAILED.rawValue,
203
+ "message": StoreError.failedVerification.localizedDescription,
204
+ "productId": "unknown"
205
+ ]
206
+ addError(error: StoreError.failedVerification, errorDict: err)
207
+ } catch {
208
+ let err = [
209
+ "responseCode": IapErrors.E_UNKNOWN.rawValue,
210
+ "debugMessage": error.localizedDescription,
211
+ "code": IapErrors.E_UNKNOWN.rawValue,
212
+ "message": error.localizedDescription,
213
+ "productId": "unknown"
214
+ ]
215
+ addError(error: error, errorDict: err)
216
+ }
217
+ }
218
+
219
+ return purchasedItems.map { serializeTransaction($0) }
220
+ }
221
+
222
+ AsyncFunction("buyProduct") { (sku: String, autoFinish: Bool, appAccountToken: String?, quantity: Int, discountOffer: [String: String]?) -> [String: Any?]? in
223
+ guard let productStore = self.productStore else {
224
+ throw NSError(domain: "ExpoIapModule", code: 1, userInfo: [NSLocalizedDescriptionKey: "Connection not initialized"])
225
+ }
226
+
227
+ let product: Product? = await productStore.getProduct(productID: sku)
228
+ if let product = product {
229
+ do {
230
+ var options: Set<Product.PurchaseOption> = []
231
+ if quantity > -1 {
232
+ options.insert(.quantity(quantity))
233
+ }
234
+ if let offerID = discountOffer?["identifier"], let keyID = discountOffer?["keyIdentifier"], let nonce = discountOffer?["nonce"], let signature = discountOffer?["signature"], let timestamp = discountOffer?["timestamp"], let uuidNonce = UUID(uuidString: nonce), let signatureData = signature.data(using: .utf8), let timestampInt = Int(timestamp) {
235
+ options.insert(.promotionalOffer(offerID: offerID, keyID: keyID, nonce: uuidNonce, signature: signatureData, timestamp: timestampInt))
236
+ }
237
+ if let appAccountToken = appAccountToken, let appAccountUUID = UUID(uuidString: appAccountToken) {
238
+ options.insert(.appAccountToken(appAccountUUID))
239
+ }
240
+ guard let windowScene = await self.currentWindowScene() else {
241
+ throw NSError(domain: "ExpoIapModule", code: 2, userInfo: [NSLocalizedDescriptionKey: "Could not find window scene"])
242
+ }
243
+ let result: Product.PurchaseResult
244
+ if #available(iOS 17.0, *) {
245
+ result = try await product.purchase(confirmIn: windowScene, options: options)
246
+ } else {
247
+ result = try await product.purchase(options: options)
248
+ }
249
+ switch result {
250
+ case .success(let verification):
251
+ let transaction = try self.checkVerified(verification)
252
+ if autoFinish {
253
+ await transaction.finish()
254
+ return nil
255
+ } else {
256
+ self.transactions[String(transaction.id)] = transaction
257
+ self.sendEvent(IapEvent.PurchaseUpdated, serializeTransaction(transaction))
258
+ return serializeTransaction(transaction)
259
+ }
260
+
261
+ case .userCancelled:
262
+ throw NSError(domain: "ExpoIapModule", code: 3, userInfo: [NSLocalizedDescriptionKey: "User cancelled the purchase"])
263
+
264
+ case .pending:
265
+ throw NSError(domain: "ExpoIapModule", code: 4, userInfo: [NSLocalizedDescriptionKey: "The payment was deferred"])
266
+ @unknown default:
267
+ throw NSError(domain: "ExpoIapModule", code: 5, userInfo: [NSLocalizedDescriptionKey: "Unknown purchase result"])
268
+ }
269
+ } catch {
270
+ throw NSError(domain: "ExpoIapModule", code: 6, userInfo: [NSLocalizedDescriptionKey: "Purchase failed: \(error.localizedDescription)"])
271
+ }
272
+ } else {
273
+ throw NSError(domain: "ExpoIapModule", code: 7, userInfo: [NSLocalizedDescriptionKey: "Invalid product ID"])
274
+ }
275
+ }
276
+
277
+ AsyncFunction("isEligibleForIntroOffer") { (groupID: String) -> Bool in
278
+ return await Product.SubscriptionInfo.isEligibleForIntroOffer(for: groupID)
279
+ }
280
+
281
+ AsyncFunction("subscriptionStatus") { (sku: String) -> [[String: Any?]?]? in
282
+ guard let productStore = self.productStore else {
283
+ throw NSError(domain: "ExpoIapModule", code: 1, userInfo: [NSLocalizedDescriptionKey: "Connection not initialized"])
284
+ }
285
+
286
+ do {
287
+ let product = await productStore.getProduct(productID: sku)
288
+ let status: [Product.SubscriptionInfo.Status]? = try await product?.subscription?.status
289
+ guard let status = status else {
290
+ return nil
291
+ }
292
+ return status.map { serializeSubscriptionStatus($0) }
293
+ } catch {
294
+ throw NSError(domain: "ExpoIapModule", code: 2, userInfo: [NSLocalizedDescriptionKey: "Error getting subscription status: \(error.localizedDescription)"])
295
+ }
296
+ }
297
+
298
+ AsyncFunction("currentEntitlement") { (sku: String) -> [String: Any?]? in
299
+ guard let productStore = self.productStore else {
300
+ throw NSError(domain: "ExpoIapModule", code: 1, userInfo: [NSLocalizedDescriptionKey: "Connection not initialized"])
301
+ }
302
+
303
+ if let product = await productStore.getProduct(productID: sku) {
304
+ if let result = await product.currentEntitlement {
305
+ do {
306
+ // Check whether the transaction is verified. If it isn’t, catch `failedVerification` error.
307
+ let transaction = try self.checkVerified(result)
308
+ return serializeTransaction(transaction)
309
+ } catch StoreError.failedVerification {
310
+ throw NSError(domain: "ExpoIapModule", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to verify transaction for sku \(sku)"])
311
+ } catch {
312
+ throw NSError(domain: "ExpoIapModule", code: 3, userInfo: [NSLocalizedDescriptionKey: "Error fetching entitlement for sku \(sku): \(error.localizedDescription)"])
313
+ }
314
+ } else {
315
+ throw NSError(domain: "ExpoIapModule", code: 4, userInfo: [NSLocalizedDescriptionKey: "Can't find entitlement for sku \(sku)"])
316
+ }
317
+ } else {
318
+ throw NSError(domain: "ExpoIapModule", code: 5, userInfo: [NSLocalizedDescriptionKey: "Can't find product for sku \(sku)"])
319
+ }
320
+ }
321
+
322
+ AsyncFunction("latestTransaction") { (sku: String) -> [String: Any?]? in
323
+ guard let productStore = self.productStore else {
324
+ throw NSError(domain: "ExpoIapModule", code: 1, userInfo: [NSLocalizedDescriptionKey: "Connection not initialized"])
325
+ }
326
+
327
+ if let product = await productStore.getProduct(productID: sku) {
328
+ if let result = await product.latestTransaction {
329
+ do {
330
+ // Check whether the transaction is verified. If it isn’t, catch `failedVerification` error.
331
+ let transaction = try self.checkVerified(result)
332
+ return serializeTransaction(transaction)
333
+ } catch StoreError.failedVerification {
334
+ throw NSError(domain: "ExpoIapModule", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to verify transaction for sku \(sku)"])
335
+ } catch {
336
+ throw NSError(domain: "ExpoIapModule", code: 3, userInfo: [NSLocalizedDescriptionKey: "Error fetching latest transaction for sku \(sku): \(error.localizedDescription)"])
337
+ }
338
+ } else {
339
+ throw NSError(domain: "ExpoIapModule", code: 4, userInfo: [NSLocalizedDescriptionKey: "Can't find latest transaction for sku \(sku)"])
340
+ }
341
+ } else {
342
+ throw NSError(domain: "ExpoIapModule", code: 5, userInfo: [NSLocalizedDescriptionKey: "Can't find product for sku \(sku)"])
343
+ }
344
+ }
345
+
346
+ AsyncFunction("finishTransaction") { (transactionIdentifier: String) -> Bool in
347
+ if let transaction = self.transactions[transactionIdentifier] {
348
+ await transaction.finish()
349
+ self.transactions.removeValue(forKey: transactionIdentifier)
350
+ return true
351
+ } else {
352
+ throw NSError(domain: "ExpoIapModule", code: 8, userInfo: [NSLocalizedDescriptionKey: "Invalid transaction ID"])
353
+ }
354
+ }
355
+
356
+ AsyncFunction("getPendingTransactions") { () -> [[String: Any?]?] in
357
+ return self.transactions.values.map { serializeTransaction($0) }
358
+ }
359
+
360
+ AsyncFunction("sync") { () -> Bool in
361
+ do {
362
+ try await AppStore.sync()
363
+ return true
364
+ } catch {
365
+ throw NSError(domain: "ExpoIapModule", code: 9, userInfo: [NSLocalizedDescriptionKey: "Error synchronizing with the AppStore: \(error.localizedDescription)"])
366
+ }
367
+ }
368
+
369
+ AsyncFunction("presentCodeRedemptionSheet") { () -> Bool in
370
+ #if !os(tvOS)
371
+ SKPaymentQueue.default().presentCodeRedemptionSheet()
372
+ return true
373
+ #else
374
+ throw NSError(domain: "ExpoIapModule", code: 10, userInfo: [NSLocalizedDescriptionKey: "This method is not available on tvOS"])
375
+ #endif
376
+ }
377
+
378
+ AsyncFunction("showManageSubscriptions") { () -> Bool in
379
+ #if !os(tvOS)
380
+ guard let windowScene = await self.currentWindowScene() else {
381
+ throw NSError(domain: "ExpoIapModule", code: 11, userInfo: [NSLocalizedDescriptionKey: "Cannot find window scene or not available on macOS"])
382
+ }
383
+ try await AppStore.showManageSubscriptions(in: windowScene)
384
+ return true
385
+ #else
386
+ throw NSError(domain: "ExpoIapModule", code: 12, userInfo: [NSLocalizedDescriptionKey: "This method is not available on tvOS"])
387
+ #endif
388
+ }
389
+
390
+ AsyncFunction("clearTransaction") { () -> Void in
391
+ Task {
392
+ for await result in Transaction.unfinished {
393
+ do {
394
+ // Check whether the transaction is verified. If it isn’t, catch `failedVerification` error.
395
+ let transaction = try self.checkVerified(result)
396
+ await transaction.finish()
397
+ self.transactions.removeValue(forKey: String(transaction.id))
398
+ } catch {
399
+ print("Failed to finish transaction")
400
+ }
401
+ }
402
+ }
403
+ }
404
+
405
+ AsyncFunction("beginRefundRequest") { (sku: String) -> String? in
406
+ #if !os(tvOS)
407
+ guard let product = await self.productStore?.getProduct(productID: sku),
408
+ let result = await product.latestTransaction else {
409
+ throw NSError(domain: "ExpoIapModule", code: 5, userInfo: [NSLocalizedDescriptionKey: "Can't find product or transaction for sku \(sku)"])
410
+ }
411
+
412
+ do {
413
+ let transaction = try self.checkVerified(result)
414
+ guard let windowScene = await self.currentWindowScene() else {
415
+ throw NSError(domain: "ExpoIapModule", code: 11, userInfo: [NSLocalizedDescriptionKey: "Cannot find window scene or not available on macOS"])
416
+ }
417
+ let refundStatus = try await transaction.beginRefundRequest(in: windowScene)
418
+ return serialize(refundStatus)
419
+ } catch StoreError.failedVerification {
420
+ throw NSError(domain: "ExpoIapModule", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to verify transaction for sku \(sku)"])
421
+ } catch {
422
+ throw NSError(domain: "ExpoIapModule", code: 3, userInfo: [NSLocalizedDescriptionKey: "Failed to refund purchase: \(error.localizedDescription)"])
423
+ }
424
+ #else
425
+ throw NSError(domain: "ExpoIapModule", code: 12, userInfo: [NSLocalizedDescriptionKey: "This method is not available on tvOS"])
426
+ #endif
427
+ }
428
+
429
+ Function("disable") {
430
+ self.removeTransactionObserver()
431
+ return true
432
+ }
433
+ }
434
+
435
+ private func addTransactionObserver() {
436
+ if updateListenerTask == nil {
437
+ updateListenerTask = listenForTransactions()
438
+ }
439
+ }
440
+
441
+ private func removeTransactionObserver() {
442
+ updateListenerTask?.cancel()
443
+ updateListenerTask = nil
444
+ }
445
+
446
+ private func listenForTransactions() -> Task<Void, Error> {
447
+ return Task.detached { [weak self] in
448
+ guard let self = self else { return }
449
+ for await result in Transaction.updates {
450
+ do {
451
+ let transaction = try self.checkVerified(result)
452
+ self.transactions[String(transaction.id)] = transaction
453
+ if self.hasListeners {
454
+ self.sendEvent(IapEvent.PurchaseUpdated, serializeTransaction(transaction))
455
+ self.sendEvent(IapEvent.TransactionIapUpdated, ["transaction": serializeTransaction(transaction)])
456
+ }
457
+ } catch {
458
+ if self.hasListeners {
459
+ let err = [
460
+ "responseCode": IapErrors.E_TRANSACTION_VALIDATION_FAILED.rawValue,
461
+ "debugMessage": error.localizedDescription,
462
+ "code": IapErrors.E_TRANSACTION_VALIDATION_FAILED.rawValue,
463
+ "message": error.localizedDescription
464
+ ]
465
+ self.sendEvent(IapEvent.PurchaseError, err)
466
+ self.sendEvent(IapEvent.TransactionIapUpdated, ["error": err])
467
+ }
468
+ }
469
+ }
470
+ }
471
+ }
472
+
473
+ public func startObserving() {
474
+ hasListeners = true
475
+ addTransactionObserver()
476
+ }
477
+
478
+ public func stopObserving() {
479
+ hasListeners = false
480
+ removeTransactionObserver()
481
+ }
482
+
483
+ private func currentWindowScene() async -> UIWindowScene? {
484
+ await MainActor.run {
485
+ return UIApplication.shared.connectedScenes.first as? UIWindowScene
486
+ }
487
+ }
488
+
489
+ private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
490
+ switch result {
491
+ case .unverified(_, let error):
492
+ throw error
493
+
494
+ case .verified(let item):
495
+ return item
496
+ }
497
+ }
498
+ }
@@ -0,0 +1,27 @@
1
+ import Foundation
2
+ import StoreKit
3
+
4
+ @available(iOS 15.0, *)
5
+ actor ProductStore {
6
+ private(set) var products: [String: Product] = [:]
7
+
8
+ func addProduct(_ product: Product) {
9
+ self.products[product.id] = product
10
+ }
11
+
12
+ func getAllProducts() -> [Product] {
13
+ return Array(self.products.values)
14
+ }
15
+
16
+ func getProduct(productID: String) -> Product? {
17
+ return self.products[productID]
18
+ }
19
+
20
+ func removeAll() {
21
+ products.removeAll()
22
+ }
23
+
24
+ func performOnActor(_ action: @escaping (isolated ProductStore) -> Void) async {
25
+ action(self)
26
+ }
27
+ }
@@ -0,0 +1,54 @@
1
+ //
2
+ // IapTypes.swift
3
+ // RNIap
4
+ //
5
+ // Created by Andres Aguilar on 8/18/22.
6
+ //
7
+
8
+ import Foundation
9
+ import StoreKit
10
+
11
+ public enum StoreError: Error {
12
+ case failedVerification
13
+ }
14
+
15
+ enum IapErrors: String, CaseIterable {
16
+ case E_UNKNOWN = "E_UNKNOWN"
17
+ case E_SERVICE_ERROR = "E_SERVICE_ERROR"
18
+ case E_USER_CANCELLED = "E_USER_CANCELLED"
19
+ case E_USER_ERROR = "E_USER_ERROR"
20
+ case E_ITEM_UNAVAILABLE = "E_ITEM_UNAVAILABLE"
21
+ case E_REMOTE_ERROR = "E_REMOTE_ERROR"
22
+ case E_NETWORK_ERROR = "E_NETWORK_ERROR"
23
+ case E_RECEIPT_FAILED = "E_RECEIPT_FAILED"
24
+ case E_RECEIPT_FINISHED_FAILED = "E_RECEIPT_FINISHED_FAILED"
25
+ case E_DEVELOPER_ERROR = "E_DEVELOPER_ERROR"
26
+ case E_PURCHASE_ERROR = "E_PURCHASE_ERROR"
27
+ case E_SYNC_ERROR = "E_SYNC_ERROR"
28
+ case E_DEFERRED_PAYMENT = "E_DEFERRED_PAYMENT"
29
+ case E_TRANSACTION_VALIDATION_FAILED = "E_TRANSACTION_VALIDATION_FAILED"
30
+ func asInt() -> Int {
31
+ return IapErrors.allCases.firstIndex(of: self)!
32
+ }
33
+ }
34
+
35
+ // Based on https://stackoverflow.com/a/40135192/570612
36
+ extension Date {
37
+ var millisecondsSince1970: Int64 {
38
+ return Int64((self.timeIntervalSince1970 * 1000.0).rounded())
39
+ }
40
+
41
+ var millisecondsSince1970String: String {
42
+ return String(self.millisecondsSince1970)
43
+ }
44
+
45
+ init(milliseconds: Int64) {
46
+ self = Date(timeIntervalSince1970: TimeInterval(milliseconds) / 1000)
47
+ }
48
+ }
49
+
50
+ extension SKProductsRequest {
51
+ var key: String {
52
+ return String(self.hashValue)
53
+ }
54
+ }