expo-iap 3.1.0 → 3.1.1-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.
- package/build/ExpoIapModule.d.ts +1 -0
- package/build/ExpoIapModule.d.ts.map +1 -1
- package/build/ExpoIapModule.js +29 -4
- package/build/ExpoIapModule.js.map +1 -1
- package/build/utils/constants.d.ts +2 -0
- package/build/utils/constants.d.ts.map +1 -1
- package/build/utils/constants.js +9 -2
- package/build/utils/constants.js.map +1 -1
- package/coverage/clover.xml +497 -0
- package/coverage/coverage-final.json +6 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +161 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +196 -0
- package/coverage/lcov-report/src/ExpoIap.types.ts.html +1243 -0
- package/coverage/lcov-report/src/PurchaseError.ts.html +787 -0
- package/coverage/lcov-report/src/helpers/index.html +116 -0
- package/coverage/lcov-report/src/helpers/subscription.ts.html +496 -0
- package/coverage/lcov-report/src/index.html +116 -0
- package/coverage/lcov-report/src/index.ts.html +1993 -0
- package/coverage/lcov-report/src/modules/android.ts.html +550 -0
- package/coverage/lcov-report/src/modules/index.html +131 -0
- package/coverage/lcov-report/src/modules/ios.ts.html +1222 -0
- package/coverage/lcov-report/src/purchase-error.ts.html +880 -0
- package/coverage/lcov-report/src/types/ExpoIapAndroid.types.ts.html +493 -0
- package/coverage/lcov-report/src/types/index.html +116 -0
- package/coverage/lcov-report/src/useIap.ts.html +1483 -0
- package/coverage/lcov-report/src/utils/errorMapping.ts.html +1069 -0
- package/coverage/lcov-report/src/utils/index.html +116 -0
- package/coverage/lcov-report/src/utils/purchase.ts.html +241 -0
- package/coverage/lcov.info +929 -0
- package/expo-module.config.json +10 -3
- package/ios/OneSideModule.swift +489 -0
- package/ios/expoiap.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/ios/expoiap.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/package.json +3 -2
- package/plugin/build/withIAP.d.ts +22 -9
- package/plugin/build/withIAP.js +157 -9
- package/plugin/jest.config.js +13 -3
- package/plugin/src/expoConfig.augmentation.d.ts +38 -0
- package/plugin/src/withIAP.ts +258 -18
- package/plugin/tsconfig.json +2 -1
- package/plugin/tsconfig.tsbuildinfo +1 -1
- package/src/ExpoIapModule.ts +45 -4
- package/src/utils/constants.ts +11 -2
package/expo-module.config.json
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
-
"platforms": [
|
|
2
|
+
"platforms": [
|
|
3
|
+
"ios",
|
|
4
|
+
"android"
|
|
5
|
+
],
|
|
3
6
|
"ios": {
|
|
4
|
-
"modules": [
|
|
7
|
+
"modules": [
|
|
8
|
+
"OneSideModule"
|
|
9
|
+
]
|
|
5
10
|
},
|
|
6
11
|
"android": {
|
|
7
|
-
"modules": [
|
|
12
|
+
"modules": [
|
|
13
|
+
"expo.modules.iap.ExpoIapModule"
|
|
14
|
+
]
|
|
8
15
|
}
|
|
9
16
|
}
|
|
@@ -0,0 +1,489 @@
|
|
|
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 OneSideBridgeError: 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 OneSideModule: 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 OneSideBridgeError.emptySkuList
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let response = try await productFetcher.fetch(identifiers: Set(request.skus))
|
|
106
|
+
|
|
107
|
+
if !response.invalidProductIdentifiers.isEmpty {
|
|
108
|
+
throw OneSideBridgeError.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 OneSideBridgeError.emptySkuList
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try await ensureProductAvailable(sku: sku)
|
|
127
|
+
guard let product = productCache[sku] else {
|
|
128
|
+
throw OneSideBridgeError.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: OneSideBridgeError.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 OneSideBridgeError.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 OneSideBridgeError.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: OneSideBridgeError.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 OneSideBridgeError.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: OneSideBridgeError.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 OneSideBridgeError.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 OneSideBridgeError.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: OneSideBridgeError.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 OneSideModule: 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 OneSideBridgeError.sdkUnavailable
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
AsyncFunction("endConnection") { () async throws -> Bool in
|
|
464
|
+
throw OneSideBridgeError.sdkUnavailable
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
AsyncFunction("fetchProducts") { (_: [String: Any]) async throws -> [[String: Any]] in
|
|
468
|
+
throw OneSideBridgeError.sdkUnavailable
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
AsyncFunction("requestPurchase") { (_: [String: Any]) async throws -> Any? in
|
|
472
|
+
throw OneSideBridgeError.sdkUnavailable
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
AsyncFunction("finishTransaction") { (_: [String: Any], _: Bool?) async throws -> Bool in
|
|
476
|
+
throw OneSideBridgeError.sdkUnavailable
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
AsyncFunction("restorePurchases") { () async throws -> Bool in
|
|
480
|
+
throw OneSideBridgeError.sdkUnavailable
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
AsyncFunction("getStorefrontIOS") { () async throws -> String in
|
|
484
|
+
throw OneSideBridgeError.sdkUnavailable
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
#endif
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-iap",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.1-rc.1",
|
|
4
4
|
"description": "In App Purchase module in Expo",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"lint:tsc": "tsc -p tsconfig.json --noEmit --skipLibCheck",
|
|
14
14
|
"lint:kt": "sh -c 'command -v ktlint >/dev/null 2>&1 && ktlint --format ./android || { echo \"ktlint not installed; skipping\"; exit 0; }'",
|
|
15
15
|
"lint:ci": "bun run lint:tsc && bun run lint:eslint && bun run lint:prettier && bun run lint:kt",
|
|
16
|
-
"test": "jest",
|
|
16
|
+
"test": "jest && bun run test:plugin",
|
|
17
|
+
"test:plugin": "cd plugin && npx jest",
|
|
17
18
|
"test:coverage": "jest --coverage",
|
|
18
19
|
"prepare": "expo-module prepare && sh -c 'command -v husky >/dev/null 2>&1 && husky install || { echo \"husky not installed; skipping\"; exit 0; }'",
|
|
19
20
|
"expo-module": "expo-module",
|
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
import { ConfigPlugin } from 'expo/config-plugins';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
import type { ExpoConfig } from '@expo/config-types';
|
|
3
|
+
import type { ExpoIapPluginCommonOptions } from './expoConfig.augmentation';
|
|
4
|
+
export declare const modifyAppBuildGradle: (gradle: string, language: "groovy" | "kotlin") => string;
|
|
5
|
+
export type AutolinkState = {
|
|
6
|
+
expoIap: boolean;
|
|
7
|
+
onside: boolean;
|
|
8
|
+
};
|
|
9
|
+
type AutolinkEntry = {
|
|
10
|
+
name: string;
|
|
11
|
+
enable: boolean;
|
|
12
|
+
};
|
|
13
|
+
export declare function computeAutolinkModules(existing: string[], desired: AutolinkEntry[]): {
|
|
14
|
+
modules: string[];
|
|
15
|
+
added: string[];
|
|
16
|
+
removed: string[];
|
|
17
|
+
};
|
|
18
|
+
export interface ModuleSelectionResult {
|
|
19
|
+
selection: 'auto' | 'expo-iap' | 'onside';
|
|
20
|
+
includeExpoIap: boolean;
|
|
21
|
+
includeOnside: boolean;
|
|
10
22
|
}
|
|
11
|
-
declare
|
|
23
|
+
export declare function resolveModuleSelection(config: ExpoConfig, options?: ExpoIapPluginCommonOptions | void): ModuleSelectionResult;
|
|
24
|
+
declare const _default: ConfigPlugin<void | ExpoIapPluginCommonOptions>;
|
|
12
25
|
export default _default;
|