expo-iap 3.1.1-rc.2 → 3.1.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 (48) hide show
  1. package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +0 -11
  2. package/build/ExpoIapModule.d.ts +0 -1
  3. package/build/ExpoIapModule.d.ts.map +1 -1
  4. package/build/ExpoIapModule.js +4 -29
  5. package/build/ExpoIapModule.js.map +1 -1
  6. package/build/useIAP.d.ts +0 -2
  7. package/build/useIAP.d.ts.map +1 -1
  8. package/build/useIAP.js +3 -8
  9. package/build/useIAP.js.map +1 -1
  10. package/expo-module.config.json +3 -10
  11. package/package.json +3 -4
  12. package/plugin/build/withIAP.d.ts +9 -22
  13. package/plugin/build/withIAP.js +9 -157
  14. package/plugin/jest.config.js +3 -13
  15. package/plugin/src/expoConfig.augmentation.d.ts +1 -30
  16. package/plugin/src/withIAP.ts +18 -258
  17. package/plugin/tsconfig.json +1 -2
  18. package/plugin/tsconfig.tsbuildinfo +1 -1
  19. package/src/ExpoIapModule.ts +4 -45
  20. package/src/useIAP.ts +3 -11
  21. package/coverage/clover.xml +0 -497
  22. package/coverage/coverage-final.json +0 -6
  23. package/coverage/lcov-report/base.css +0 -224
  24. package/coverage/lcov-report/block-navigation.js +0 -87
  25. package/coverage/lcov-report/favicon.png +0 -0
  26. package/coverage/lcov-report/index.html +0 -161
  27. package/coverage/lcov-report/prettify.css +0 -1
  28. package/coverage/lcov-report/prettify.js +0 -2
  29. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  30. package/coverage/lcov-report/sorter.js +0 -196
  31. package/coverage/lcov-report/src/ExpoIap.types.ts.html +0 -1243
  32. package/coverage/lcov-report/src/PurchaseError.ts.html +0 -787
  33. package/coverage/lcov-report/src/helpers/index.html +0 -116
  34. package/coverage/lcov-report/src/helpers/subscription.ts.html +0 -496
  35. package/coverage/lcov-report/src/index.html +0 -116
  36. package/coverage/lcov-report/src/index.ts.html +0 -1993
  37. package/coverage/lcov-report/src/modules/android.ts.html +0 -550
  38. package/coverage/lcov-report/src/modules/index.html +0 -131
  39. package/coverage/lcov-report/src/modules/ios.ts.html +0 -1222
  40. package/coverage/lcov-report/src/purchase-error.ts.html +0 -880
  41. package/coverage/lcov-report/src/types/ExpoIapAndroid.types.ts.html +0 -493
  42. package/coverage/lcov-report/src/types/index.html +0 -116
  43. package/coverage/lcov-report/src/useIap.ts.html +0 -1483
  44. package/coverage/lcov-report/src/utils/errorMapping.ts.html +0 -1069
  45. package/coverage/lcov-report/src/utils/index.html +0 -116
  46. package/coverage/lcov-report/src/utils/purchase.ts.html +0 -241
  47. package/coverage/lcov.info +0 -929
  48. package/ios/onside/OnsideIapModule.swift +0 -489
@@ -1,489 +0,0 @@
1
- import ExpoModulesCore
2
- import Foundation
3
- import OpenIAP
4
-
5
- private enum OnsideEvent: String {
6
- case purchaseUpdated = "purchase-updated"
7
- case purchaseError = "purchase-error"
8
- case promotedProductIOS = "promoted-product-ios"
9
- }
10
-
11
- private enum OnsideBridgeError: Error, LocalizedError {
12
- case sdkUnavailable
13
- case notInitialized
14
- case emptySkuList
15
- case productNotFound(String)
16
- case transactionNotFound(String)
17
- case restoreInProgress
18
- case queueError(String)
19
-
20
- var errorDescription: String? {
21
- switch self {
22
- case .sdkUnavailable:
23
- return "OnsideKit is not installed. Enable ios.onside.enabled to use this functionality."
24
- case .notInitialized:
25
- return "Connection not initialized. Call initConnection() first."
26
- case .emptySkuList:
27
- return "No product identifiers provided."
28
- case .productNotFound(let sku):
29
- return "Product with identifier \(sku) was not fetched. Call fetchProducts() first."
30
- case .transactionNotFound(let id):
31
- return "Could not locate transaction with id \(id)."
32
- case .restoreInProgress:
33
- return "A restore operation is already in progress."
34
- case .queueError(let message):
35
- return message
36
- }
37
- }
38
- }
39
-
40
- #if canImport(OnsideKit)
41
- import OnsideKit
42
-
43
- @available(iOS 16.0, *)
44
- @MainActor
45
- public final class OnsideIapModule: Module {
46
- private var isInitialized = false
47
- private var restoreContinuation: CheckedContinuation<Bool, Error>?
48
- private let transactionObserver = OnsideTransactionObserverBridge()
49
- private let productFetcher = OnsideProductFetcher()
50
- private var productCache: [String: OnsideProduct] = [:]
51
- private var transactionCache: [UUID: OnsidePaymentTransaction] = [:]
52
-
53
- private let encoder: JSONEncoder = {
54
- let encoder = JSONEncoder()
55
- encoder.dateEncodingStrategy = .millisecondsSince1970
56
- return encoder
57
- }()
58
-
59
- nonisolated public func definition() -> ModuleDefinition {
60
- Name("ExpoIapOnside")
61
-
62
- Constants {
63
- OpenIapSerialization.errorCodes()
64
- }
65
-
66
- Events(
67
- OnsideEvent.purchaseUpdated.rawValue,
68
- OnsideEvent.purchaseError.rawValue,
69
- OnsideEvent.promotedProductIOS.rawValue
70
- )
71
-
72
- OnCreate {
73
- Task { @MainActor in
74
- self.configureObserverCallbacks()
75
- }
76
- }
77
-
78
- OnDestroy {
79
- Task { @MainActor in
80
- self.cleanup()
81
- }
82
- }
83
-
84
- AsyncFunction("initConnection") { () async throws -> Bool in
85
- ExpoIapLog.payload("initConnectionOnside", payload: nil)
86
- try await ensureObserverRegistered()
87
- return true
88
- }
89
-
90
- AsyncFunction("endConnection") { () async throws -> Bool in
91
- ExpoIapLog.payload("endConnectionOnside", payload: nil)
92
- cleanup()
93
- return true
94
- }
95
-
96
- AsyncFunction("fetchProducts") { (params: [String: Any]) async throws -> [[String: Any]] in
97
- ExpoIapLog.payload("fetchProductsOnside", payload: params)
98
- try await ensureObserverRegistered()
99
-
100
- let request = try ExpoIapHelper.decodeProductRequest(from: params)
101
- guard !request.skus.isEmpty else {
102
- throw OnsideBridgeError.emptySkuList
103
- }
104
-
105
- let response = try await productFetcher.fetch(identifiers: Set(request.skus))
106
-
107
- if !response.invalidProductIdentifiers.isEmpty {
108
- throw OnsideBridgeError.productNotFound(response.invalidProductIdentifiers.joined(separator: ", "))
109
- }
110
-
111
- response.products.forEach { productCache[$0.productIdentifier] = $0 }
112
-
113
- let payload = try response.products.map { try serializeProduct($0) }
114
- ExpoIapLog.result("fetchProductsOnside", value: payload)
115
- return payload
116
- }
117
-
118
- AsyncFunction("requestPurchase") { (payload: [String: Any]) async throws -> Any? in
119
- ExpoIapLog.payload("requestPurchaseOnside", payload: payload)
120
- try await ensureObserverRegistered()
121
-
122
- guard let sku = resolveSku(from: payload) else {
123
- throw OnsideBridgeError.emptySkuList
124
- }
125
-
126
- try await ensureProductAvailable(sku: sku)
127
- guard let product = productCache[sku] else {
128
- throw OnsideBridgeError.productNotFound(sku)
129
- }
130
-
131
- try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
132
- Onside.paymentQueue().add(product) { result in
133
- switch result {
134
- case .success:
135
- continuation.resume()
136
- case .failure(let error):
137
- continuation.resume(throwing: OnsideBridgeError.queueError(error.localizedDescription))
138
- }
139
- }
140
- }
141
-
142
- ExpoIapLog.result("requestPurchaseOnside", value: nil as Any?)
143
- return nil
144
- }
145
-
146
- AsyncFunction("finishTransaction") {
147
- (purchasePayload: [String: Any], _: Bool?) async throws -> Bool in
148
- ExpoIapLog.payload("finishTransactionOnside", payload: purchasePayload)
149
- try await ensureObserverRegistered()
150
-
151
- guard let transactionId = purchasePayload["transactionId"] as? String,
152
- let uuid = UUID(uuidString: transactionId),
153
- let transaction = transactionCache[uuid] else {
154
- throw OnsideBridgeError.transactionNotFound(purchasePayload["transactionId"] as? String ?? "")
155
- }
156
-
157
- Onside.paymentQueue().finishTransaction(transaction)
158
- transactionCache.removeValue(forKey: uuid)
159
- ExpoIapLog.result("finishTransactionOnside", value: true)
160
- return true
161
- }
162
-
163
- AsyncFunction("restorePurchases") { () async throws -> Bool in
164
- ExpoIapLog.payload("restorePurchasesOnside", payload: nil)
165
- try await ensureObserverRegistered()
166
-
167
- if restoreContinuation != nil {
168
- throw OnsideBridgeError.restoreInProgress
169
- }
170
-
171
- return try await withCheckedThrowingContinuation { continuation in
172
- restoreContinuation = continuation
173
- Onside.paymentQueue().restoreCompletedTransactions { [weak self] result in
174
- guard let self else {
175
- continuation.resume(returning: true)
176
- return
177
- }
178
- switch result {
179
- case .success:
180
- continuation.resume(returning: true)
181
- case .failure(let error):
182
- continuation.resume(throwing: OnsideBridgeError.queueError(error.localizedDescription))
183
- }
184
- restoreContinuation = nil
185
- }
186
- }
187
- }
188
-
189
- AsyncFunction("getStorefrontIOS") { () async throws -> String in
190
- ExpoIapLog.payload("getStorefrontOnside", payload: nil)
191
- try await ensureObserverRegistered()
192
- let storefront = Onside.paymentQueue().storefront?.countryCode ?? ""
193
- ExpoIapLog.result("getStorefrontOnside", value: storefront)
194
- return storefront
195
- }
196
- }
197
-
198
- private func ensureObserverRegistered() async throws {
199
- if !isInitialized {
200
- Onside.paymentQueue().add(observer: transactionObserver)
201
- isInitialized = true
202
- }
203
- }
204
-
205
- private func ensureProductAvailable(sku: String) async throws {
206
- if productCache[sku] != nil {
207
- return
208
- }
209
- let response = try await productFetcher.fetch(identifiers: [sku])
210
- if !response.invalidProductIdentifiers.isEmpty {
211
- throw OnsideBridgeError.productNotFound(sku)
212
- }
213
- response.products.forEach { productCache[$0.productIdentifier] = $0 }
214
- }
215
-
216
- private func configureObserverCallbacks() {
217
- transactionObserver.onTransactionsUpdated = { [weak self] transactions in
218
- guard let self else { return }
219
- transactions.forEach { transaction in
220
- transactionCache[transaction.id] = transaction
221
- handle(transaction: transaction)
222
- }
223
- }
224
-
225
- transactionObserver.onRestoreFinished = { [weak self] in
226
- guard let self else { return }
227
- restoreContinuation?.resume(returning: true)
228
- restoreContinuation = nil
229
- }
230
-
231
- transactionObserver.onRestoreFailed = { [weak self] error in
232
- guard let self else { return }
233
- restoreContinuation?.resume(throwing: OnsideBridgeError.queueError(error.localizedDescription))
234
- restoreContinuation = nil
235
- }
236
- }
237
-
238
- private func cleanup() {
239
- if isInitialized {
240
- Onside.paymentQueue().remove(observer: transactionObserver)
241
- isInitialized = false
242
- }
243
- transactionCache.removeAll()
244
- restoreContinuation?.resume(returning: false)
245
- restoreContinuation = nil
246
- }
247
-
248
- private func handle(transaction: OnsidePaymentTransaction) {
249
- do {
250
- let payload = try serialize(transaction: transaction)
251
- switch transaction.transactionState {
252
- case .purchased, .restored:
253
- sendEvent(OnsideEvent.purchaseUpdated.rawValue, payload)
254
- case .failed:
255
- let errorPayload: [String: Any] = [
256
- "code": ErrorCode.PurchaseError.rawValue,
257
- "message": (transaction.error?.localizedDescription ?? "Purchase failed"),
258
- "productId": transaction.product.productIdentifier
259
- ]
260
- sendEvent(OnsideEvent.purchaseError.rawValue, errorPayload)
261
- case .purchasing:
262
- break
263
- @unknown default:
264
- break
265
- }
266
- } catch {
267
- ExpoIapLog.failure("handleTransactionOnside", error: error)
268
- }
269
- }
270
-
271
- private func serializeProduct(_ product: OnsideProduct) throws -> [String: Any] {
272
- var dictionary: [String: Any?] = [:]
273
- dictionary["id"] = product.productIdentifier
274
- dictionary["platform"] = "ios"
275
- dictionary["title"] = product.localizedTitle
276
- dictionary["description"] = product.localizedDescription
277
- dictionary["displayName"] = product.localizedTitle
278
- dictionary["displayNameIOS"] = product.localizedTitle
279
- dictionary["displayPrice"] = product.price.formatted
280
- dictionary["currency"] = product.price.currencyCode
281
- dictionary["price"] = product.price.value
282
- dictionary["type"] = "in-app"
283
- dictionary["typeIOS"] = "non-consumable"
284
- dictionary["isFamilyShareableIOS"] = false
285
- dictionary["jsonRepresentationIOS"] = try encodeToJSONString(product)
286
- dictionary["debugDescription"] = product.description
287
- return sanitize(dictionary)
288
- }
289
-
290
- private func serialize(transaction: OnsidePaymentTransaction) throws -> [String: Any] {
291
- let product = transaction.product
292
- var dictionary: [String: Any?] = [:]
293
- dictionary["id"] = transaction.id.uuidString
294
- dictionary["transactionId"] = transaction.id.uuidString
295
- dictionary["productId"] = product.productIdentifier
296
- dictionary["platform"] = "ios"
297
- dictionary["quantity"] = 1
298
- dictionary["isAutoRenewing"] = false
299
- dictionary["purchaseState"] = mapPurchaseState(transaction.transactionState)
300
- dictionary["transactionDate"] = Int(Date().timeIntervalSince1970 * 1000)
301
- dictionary["currencyCodeIOS"] = product.price.currencyCode
302
- dictionary["currencySymbolIOS"] = product.price.formatted
303
- dictionary["storefrontCountryCodeIOS"] = transaction.storefront.countryCode
304
- dictionary["purchaseToken"] = nil
305
- dictionary["environmentIOS"] = transaction.storefront.id
306
- if let error = transaction.error {
307
- dictionary["reasonIOS"] = error.localizedDescription
308
- }
309
- return sanitize(dictionary)
310
- }
311
-
312
- private func encodeToJSONString<T: Encodable>(_ value: T) throws -> String {
313
- let data = try encoder.encode(value)
314
- guard let json = String(data: data, encoding: .utf8) else {
315
- throw OnsideBridgeError.queueError("Unable to encode JSON string")
316
- }
317
- return json
318
- }
319
-
320
- private func sanitize(_ dictionary: [String: Any?]) -> [String: Any] {
321
- var result: [String: Any] = [:]
322
- for (key, value) in dictionary {
323
- if let value {
324
- result[key] = value
325
- }
326
- }
327
- return result
328
- }
329
-
330
- private func mapPurchaseState(_ state: OnsidePaymentTransactionState) -> String {
331
- switch state {
332
- case .purchased:
333
- return "purchased"
334
- case .restored:
335
- return "restored"
336
- case .failed:
337
- return "failed"
338
- case .purchasing:
339
- return "pending"
340
- @unknown default:
341
- return "unknown"
342
- }
343
- }
344
-
345
- private func resolveSku(from payload: [String: Any]) -> String? {
346
- if let sku = payload["sku"] as? String, !sku.isEmpty {
347
- return sku
348
- }
349
-
350
- if let request = payload["request"] as? [String: Any] {
351
- if let ios = request["ios"] as? [String: Any] {
352
- if let sku = ios["sku"] as? String, !sku.isEmpty {
353
- return sku
354
- }
355
- if let skus = ios["skus"] as? [String], let first = skus.first, !first.isEmpty {
356
- return first
357
- }
358
- }
359
- }
360
-
361
- if let requestPurchase = payload["requestPurchase"] as? [String: Any],
362
- let ios = requestPurchase["ios"] as? [String: Any],
363
- let sku = ios["sku"] as? String, !sku.isEmpty {
364
- return sku
365
- }
366
-
367
- if let requestSubscription = payload["requestSubscription"] as? [String: Any],
368
- let ios = requestSubscription["ios"] as? [String: Any],
369
- let sku = ios["sku"] as? String, !sku.isEmpty {
370
- return sku
371
- }
372
-
373
- if let skus = payload["skus"] as? [String], let first = skus.first, !first.isEmpty {
374
- return first
375
- }
376
-
377
- return nil
378
- }
379
- }
380
-
381
- @available(iOS 16.0, *)
382
- private final class OnsideTransactionObserverBridge: NSObject, OnsidePaymentTransactionObserver {
383
- var onTransactionsUpdated: (([OnsidePaymentTransaction]) -> Void)?
384
- var onRestoreFinished: (() -> Void)?
385
- var onRestoreFailed: ((OnsideTransactionsRestoreError) -> Void)?
386
-
387
- func onsidePaymentQueue(_ queue: OnsidePaymentQueue, updatedTransactions transactions: [OnsidePaymentTransaction]) {
388
- onTransactionsUpdated?(transactions)
389
- }
390
-
391
- func onsidePaymentQueue(_ queue: OnsidePaymentQueue, removedTransactions: [OnsidePaymentTransaction]) {}
392
-
393
- func onsidePaymentQueueRestoreCompletedTransactionsFinished(_ queue: OnsidePaymentQueue) {
394
- onRestoreFinished?()
395
- }
396
-
397
- func onsidePaymentQueue(_ queue: OnsidePaymentQueue, restoreCompletedTransactionsFailedWithError error: OnsideTransactionsRestoreError) {
398
- onRestoreFailed?(error)
399
- }
400
-
401
- func onsidePaymentQueueDidChangeStorefront(_ queue: OnsidePaymentQueue) {}
402
- }
403
-
404
- @available(iOS 16.0, *)
405
- private final class OnsideProductFetcher: NSObject, OnsideProductsRequestDelegate {
406
- private var continuation: CheckedContinuation<OnsideProductsResponse, Error>?
407
- private var request: OnsideProductsRequest?
408
-
409
- func fetch(identifiers: Set<String>) async throws -> OnsideProductsResponse {
410
- guard !identifiers.isEmpty else {
411
- throw OnsideBridgeError.emptySkuList
412
- }
413
-
414
- return try await withCheckedThrowingContinuation { continuation in
415
- let request = Onside.makeProductsRequest(productIdentifiers: identifiers)
416
- self.request = request
417
- self.continuation = continuation
418
- request.delegate = self
419
- request.start()
420
- }
421
- }
422
-
423
- func onsideProductsRequest(_ request: OnsideProductsRequest, didReceive response: OnsideProductsResponse) {
424
- continuation?.resume(returning: response)
425
- cleanup()
426
- }
427
-
428
- func onsideProductsRequestRequest(_ request: OnsideProductsRequest, didFailWithError error: OnsideProductsRequestError) {
429
- continuation?.resume(throwing: OnsideBridgeError.queueError(error.localizedDescription))
430
- cleanup()
431
- }
432
-
433
- func onsideProductsRequestDidFinish(_ request: OnsideProductsRequest) {
434
- cleanup()
435
- }
436
-
437
- private func cleanup() {
438
- request?.delegate = nil
439
- request?.stop()
440
- request = nil
441
- continuation = nil
442
- }
443
- }
444
-
445
- #else
446
-
447
- @available(iOS 15.0, tvOS 15.0, *)
448
- @MainActor
449
- public final class OnsideIapModule: Module {
450
- nonisolated public func definition() -> ModuleDefinition {
451
- Name("ExpoIapOnside")
452
-
453
- Events(
454
- OnsideEvent.purchaseUpdated.rawValue,
455
- OnsideEvent.purchaseError.rawValue,
456
- OnsideEvent.promotedProductIOS.rawValue
457
- )
458
-
459
- AsyncFunction("initConnection") { () async throws -> Bool in
460
- throw OnsideBridgeError.sdkUnavailable
461
- }
462
-
463
- AsyncFunction("endConnection") { () async throws -> Bool in
464
- throw OnsideBridgeError.sdkUnavailable
465
- }
466
-
467
- AsyncFunction("fetchProducts") { (_: [String: Any]) async throws -> [[String: Any]] in
468
- throw OnsideBridgeError.sdkUnavailable
469
- }
470
-
471
- AsyncFunction("requestPurchase") { (_: [String: Any]) async throws -> Any? in
472
- throw OnsideBridgeError.sdkUnavailable
473
- }
474
-
475
- AsyncFunction("finishTransaction") { (_: [String: Any], _: Bool?) async throws -> Bool in
476
- throw OnsideBridgeError.sdkUnavailable
477
- }
478
-
479
- AsyncFunction("restorePurchases") { () async throws -> Bool in
480
- throw OnsideBridgeError.sdkUnavailable
481
- }
482
-
483
- AsyncFunction("getStorefrontIOS") { () async throws -> String in
484
- throw OnsideBridgeError.sdkUnavailable
485
- }
486
- }
487
- }
488
-
489
- #endif