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.
- package/.eslintrc.js +9 -0
- package/.prettierrc.js +9 -0
- package/.swiftlint.yml +10 -0
- package/README.md +28 -21
- package/android/build.gradle +34 -72
- package/android/src/main/AndroidManifest.xml +1 -4
- package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +535 -0
- package/android/src/main/java/expo/modules/iap/MissingCurrentActivityException.kt +6 -0
- package/android/src/main/java/expo/modules/iap/PlayUtils.kt +124 -0
- package/build/ExpoIap.types.d.ts +89 -0
- package/build/ExpoIap.types.d.ts.map +1 -0
- package/build/ExpoIap.types.js +59 -0
- package/build/ExpoIap.types.js.map +1 -0
- package/build/ExpoIapModule.d.ts +3 -0
- package/build/ExpoIapModule.d.ts.map +1 -0
- package/build/ExpoIapModule.js +5 -0
- package/build/ExpoIapModule.js.map +1 -0
- package/build/index.d.ts +38 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +202 -0
- package/build/index.js.map +1 -0
- package/build/modules/android.d.ts +40 -0
- package/build/modules/android.d.ts.map +1 -0
- package/build/modules/android.js +54 -0
- package/build/modules/android.js.map +1 -0
- package/build/modules/ios.d.ts +41 -0
- package/build/modules/ios.d.ts.map +1 -0
- package/build/modules/ios.js +44 -0
- package/build/modules/ios.js.map +1 -0
- package/build/types/ExpoIapAndroid.types.d.ts +113 -0
- package/build/types/ExpoIapAndroid.types.d.ts.map +1 -0
- package/build/types/ExpoIapAndroid.types.js +23 -0
- package/build/types/ExpoIapAndroid.types.js.map +1 -0
- package/build/types/ExpoIapIos.types.d.ts +122 -0
- package/build/types/ExpoIapIos.types.d.ts.map +1 -0
- package/build/types/ExpoIapIos.types.js +2 -0
- package/build/types/ExpoIapIos.types.js.map +1 -0
- package/bun.lockb +0 -0
- package/expo-module.config.json +4 -8
- package/ios/ExpoIap.podspec +27 -0
- package/ios/ExpoIapModule.swift +498 -0
- package/ios/ProductStore.swift +27 -0
- package/ios/Types.swift +54 -0
- package/package.json +33 -62
- package/src/ExpoIap.types.ts +125 -0
- package/src/ExpoIapModule.ts +5 -0
- package/src/index.ts +397 -0
- package/src/modules/android.ts +89 -0
- package/src/modules/ios.ts +81 -0
- package/src/types/ExpoIapAndroid.types.ts +123 -0
- package/src/types/ExpoIapIos.types.ts +141 -0
- package/tsconfig.json +9 -0
- package/.editorconfig +0 -10
- package/.flowconfig +0 -11
- package/.monolinterrc +0 -3
- package/.yarn/install-state.gz +0 -0
- package/.yarn/releases/yarn-3.1.1.cjs +0 -768
- package/.yarnrc.yml +0 -3
- package/LICENSE +0 -21
- package/RNIap.podspec +0 -18
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +0 -6
- package/android/gradle.properties +0 -2
- package/android/gradlew +0 -160
- package/android/gradlew.bat +0 -90
- package/android/libs/in-app-purchasing-2.0.76.jar +0 -0
- package/android/src/amazon/AndroidManifest.xml +0 -12
- package/android/src/amazon/java/com/dooboolab/RNIap/RNIapAmazonListener.kt +0 -356
- package/android/src/amazon/java/com/dooboolab/RNIap/RNIapAmazonModule.kt +0 -128
- package/android/src/amazon/java/com/dooboolab/RNIap/RNIapPackage.kt +0 -20
- package/android/src/main/java/com/dooboolab/RNIap/DoobooUtils.kt +0 -180
- package/android/src/play/java/com/dooboolab/RNIap/PlayUtils.kt +0 -77
- package/android/src/play/java/com/dooboolab/RNIap/RNIapModule.kt +0 -698
- package/android/src/play/java/com/dooboolab/RNIap/RNIapPackage.kt +0 -20
- package/babel.config.js +0 -10
- package/index.d.ts +0 -3
- package/index.js +0 -3
- package/index.js.flow +0 -9
- package/ios/RNIap.xcodeproj/project.pbxproj +0 -370
- package/ios/RNIap.xcodeproj/xcshareddata/xcschemes/RNIap.xcscheme +0 -80
- package/ios/RNIapIos.m +0 -60
- package/ios/RNIapIos.swift +0 -932
- package/ios/RNIapQueue.swift +0 -35
- package/jest.config.js +0 -194
- package/src/__test__/iap.test.d.ts +0 -1
- package/src/__test__/iap.test.js +0 -59
- package/src/hooks/useIAP.d.ts +0 -21
- package/src/hooks/useIAP.js +0 -140
- package/src/hooks/withIAPContext.d.ts +0 -21
- package/src/hooks/withIAPContext.js +0 -142
- package/src/iap.d.ts +0 -197
- package/src/iap.js +0 -625
- package/src/index.d.ts +0 -4
- package/src/index.js +0 -4
- package/src/types/amazon.d.ts +0 -23
- package/src/types/amazon.js +0 -1
- package/src/types/android.d.ts +0 -47
- package/src/types/android.js +0 -22
- package/src/types/apple.d.ts +0 -424
- package/src/types/apple.js +0 -165
- package/src/types/index.d.ts +0 -117
- package/src/types/index.js +0 -40
- 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
|
+
}
|
package/ios/Types.swift
ADDED
|
@@ -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
|
+
}
|