react-native-iap 14.1.1-rc.1 → 14.2.0
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/NitroIap.podspec +2 -0
- package/README.md +9 -9
- package/android/build.gradle +2 -1
- package/android/consumer-rules.pro +7 -0
- package/app.plugin.js +1 -1
- package/ios/HybridRnIap.swift +380 -1192
- package/lib/module/helpers/subscription.js.map +1 -1
- package/lib/module/hooks/useIAP.js +74 -58
- package/lib/module/hooks/useIAP.js.map +1 -1
- package/lib/module/index.js +20 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/types.js +8 -0
- package/lib/module/types.js.map +1 -1
- package/lib/module/utils/error.js.map +1 -1
- package/lib/module/utils/errorMapping.js +33 -0
- package/lib/module/utils/errorMapping.js.map +1 -0
- package/lib/module/utils/type-bridge.js +19 -0
- package/lib/module/utils/type-bridge.js.map +1 -1
- package/lib/typescript/src/helpers/subscription.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useIAP.d.ts +4 -4
- package/lib/typescript/src/hooks/useIAP.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +7 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +19 -0
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/utils/error.d.ts.map +1 -1
- package/lib/typescript/src/utils/errorMapping.d.ts +5 -0
- package/lib/typescript/src/utils/errorMapping.d.ts.map +1 -0
- package/lib/typescript/src/utils/type-bridge.d.ts +3 -2
- package/lib/typescript/src/utils/type-bridge.d.ts.map +1 -1
- package/package.json +5 -2
- package/plugin/tsconfig.tsbuildinfo +1 -1
- package/src/helpers/subscription.ts +30 -30
- package/src/hooks/useIAP.ts +252 -230
- package/src/index.ts +366 -340
- package/src/types.ts +21 -0
- package/src/utils/error.ts +19 -19
- package/src/utils/errorMapping.ts +44 -0
- package/src/utils/type-bridge.ts +127 -93
- package/ios/ErrorUtils.swift +0 -153
- package/ios/ProductStore.swift +0 -43
- package/ios/reactnativeiap.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
- package/ios/reactnativeiap.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
- package/plugin/build/src/withIAP.d.ts +0 -3
- package/plugin/build/src/withIAP.js +0 -81
- package/plugin/build/tsconfig.tsbuildinfo +0 -1
package/ios/HybridRnIap.swift
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import NitroModules
|
|
3
|
-
import
|
|
4
|
-
#if os(iOS) || os(tvOS)
|
|
5
|
-
import UIKit
|
|
6
|
-
#endif
|
|
3
|
+
import OpenIAP
|
|
7
4
|
|
|
8
5
|
@available(iOS 15.0, *)
|
|
9
6
|
class HybridRnIap: HybridRnIapSpec {
|
|
@@ -11,14 +8,14 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
11
8
|
|
|
12
9
|
private var updateListenerTask: Task<Void, Never>?
|
|
13
10
|
private var isInitialized: Bool = false
|
|
14
|
-
private var
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
private var isInitializing: Bool = false
|
|
12
|
+
// No local StoreKit cache; rely on OpenIAP
|
|
13
|
+
// OpenIAP event subscriptions
|
|
14
|
+
private var purchaseUpdatedSub: Subscription?
|
|
15
|
+
private var purchaseErrorSub: Subscription?
|
|
16
|
+
private var promotedProductSub: Subscription?
|
|
17
17
|
|
|
18
|
-
// Promoted products
|
|
19
|
-
private var promotedProduct: Product?
|
|
20
|
-
private var promotedPayment: SKPayment?
|
|
21
|
-
private var promotedSKProduct: SKProduct?
|
|
18
|
+
// Promoted products are handled via OpenIAP listener
|
|
22
19
|
|
|
23
20
|
// Event listeners
|
|
24
21
|
private var purchaseUpdatedListeners: [(NitroPurchase) -> Void] = []
|
|
@@ -36,32 +33,136 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
36
33
|
}
|
|
37
34
|
|
|
38
35
|
// MARK: - Public Methods (Cross-platform)
|
|
36
|
+
|
|
37
|
+
|
|
39
38
|
|
|
40
39
|
func initConnection() throws -> Promise<Bool> {
|
|
41
40
|
return Promise.async {
|
|
42
|
-
//
|
|
43
|
-
self.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if self.
|
|
54
|
-
self.
|
|
55
|
-
|
|
41
|
+
// If already initialized or initializing, ensure listeners are attached and return immediately
|
|
42
|
+
if self.isInitialized || self.isInitializing {
|
|
43
|
+
if self.purchaseUpdatedSub == nil {
|
|
44
|
+
self.purchaseUpdatedSub = OpenIapModule.shared.purchaseUpdatedListener { [weak self] openIapPurchase in
|
|
45
|
+
guard let self else { return }
|
|
46
|
+
Task { @MainActor in
|
|
47
|
+
let nitro = self.convertOpenIapPurchaseToNitroPurchase(openIapPurchase)
|
|
48
|
+
self.sendPurchaseUpdate(nitro)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if self.purchaseErrorSub == nil {
|
|
53
|
+
self.purchaseErrorSub = OpenIapModule.shared.purchaseErrorListener { [weak self] error in
|
|
54
|
+
guard let self else { return }
|
|
55
|
+
Task { @MainActor in
|
|
56
|
+
let nitroError = self.createPurchaseErrorResult(
|
|
57
|
+
code: error.code,
|
|
58
|
+
message: error.message,
|
|
59
|
+
productId: error.productId
|
|
60
|
+
)
|
|
61
|
+
self.sendPurchaseError(nitroError)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if self.promotedProductSub == nil {
|
|
66
|
+
self.promotedProductSub = OpenIapModule.shared.promotedProductListenerIOS { [weak self] productId in
|
|
67
|
+
guard let self else { return }
|
|
68
|
+
Task {
|
|
69
|
+
do {
|
|
70
|
+
let req = OpenIapProductRequest(skus: [productId], type: "all")
|
|
71
|
+
let products = try await OpenIapModule.shared.fetchProducts(req)
|
|
72
|
+
if let p = products.first {
|
|
73
|
+
let nitro = self.convertOpenIapProductToNitroProduct(p)
|
|
74
|
+
await MainActor.run {
|
|
75
|
+
for listener in self.promotedProductListeners { listener(nitro) }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
let id = productId
|
|
80
|
+
await MainActor.run {
|
|
81
|
+
var minimal = NitroProduct()
|
|
82
|
+
minimal.id = id
|
|
83
|
+
minimal.title = id
|
|
84
|
+
minimal.type = "inapp"
|
|
85
|
+
minimal.platform = "ios"
|
|
86
|
+
for listener in self.promotedProductListeners { listener(minimal) }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return true
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Begin non-blocking initialization
|
|
96
|
+
self.isInitializing = true
|
|
97
|
+
// Pre-attach listeners so events are not missed
|
|
98
|
+
if self.purchaseUpdatedSub == nil {
|
|
99
|
+
self.purchaseUpdatedSub = OpenIapModule.shared.purchaseUpdatedListener { [weak self] openIapPurchase in
|
|
100
|
+
guard let self else { return }
|
|
101
|
+
Task { @MainActor in
|
|
102
|
+
let nitro = self.convertOpenIapPurchaseToNitroPurchase(openIapPurchase)
|
|
103
|
+
self.sendPurchaseUpdate(nitro)
|
|
104
|
+
}
|
|
56
105
|
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
106
|
+
}
|
|
107
|
+
if self.purchaseErrorSub == nil {
|
|
108
|
+
self.purchaseErrorSub = OpenIapModule.shared.purchaseErrorListener { [weak self] error in
|
|
109
|
+
guard let self else { return }
|
|
110
|
+
Task { @MainActor in
|
|
111
|
+
let nitroError = self.createPurchaseErrorResult(
|
|
112
|
+
code: error.code,
|
|
113
|
+
message: error.message,
|
|
114
|
+
productId: error.productId
|
|
115
|
+
)
|
|
116
|
+
self.sendPurchaseError(nitroError)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if self.promotedProductSub == nil {
|
|
121
|
+
self.promotedProductSub = OpenIapModule.shared.promotedProductListenerIOS { [weak self] productId in
|
|
122
|
+
guard let self else { return }
|
|
123
|
+
Task {
|
|
124
|
+
do {
|
|
125
|
+
let req = OpenIapProductRequest(skus: [productId], type: "all")
|
|
126
|
+
let products = try await OpenIapModule.shared.fetchProducts(req)
|
|
127
|
+
if let p = products.first {
|
|
128
|
+
let nitro = self.convertOpenIapProductToNitroProduct(p)
|
|
129
|
+
await MainActor.run {
|
|
130
|
+
for listener in self.promotedProductListeners { listener(nitro) }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
let id = productId
|
|
135
|
+
await MainActor.run {
|
|
136
|
+
var minimal = NitroProduct()
|
|
137
|
+
minimal.id = id
|
|
138
|
+
minimal.title = id
|
|
139
|
+
minimal.type = "inapp"
|
|
140
|
+
minimal.platform = "ios"
|
|
141
|
+
for listener in self.promotedProductListeners { listener(minimal) }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Perform initialization and only report success when native init succeeds
|
|
149
|
+
do {
|
|
150
|
+
let ok = try await OpenIapModule.shared.initConnection()
|
|
151
|
+
self.isInitialized = ok
|
|
152
|
+
self.isInitializing = false
|
|
153
|
+
return ok
|
|
154
|
+
} catch {
|
|
155
|
+
// Surface as event and keep flags consistent
|
|
156
|
+
let err = self.createPurchaseErrorResult(
|
|
157
|
+
code: OpenIapError.E_INIT_CONNECTION,
|
|
158
|
+
message: error.localizedDescription,
|
|
159
|
+
productId: nil
|
|
61
160
|
)
|
|
62
|
-
|
|
161
|
+
self.sendPurchaseError(err)
|
|
162
|
+
self.isInitialized = false
|
|
163
|
+
self.isInitializing = false
|
|
164
|
+
return false
|
|
63
165
|
}
|
|
64
|
-
return canMakePayments
|
|
65
166
|
}
|
|
66
167
|
}
|
|
67
168
|
|
|
@@ -76,144 +177,52 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
76
177
|
return Promise.async {
|
|
77
178
|
do {
|
|
78
179
|
try self.ensureConnection()
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
print("[RnIap] StoreKit returned \(storeProducts.count) products")
|
|
84
|
-
|
|
85
|
-
// Store products in ProductStore
|
|
86
|
-
if let productStore = self.productStore {
|
|
87
|
-
await productStore.addProducts(storeProducts)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Convert StoreKit products to NitroProduct
|
|
91
|
-
let nitroProducts = storeProducts.map { storeProduct in
|
|
92
|
-
print("[RnIap] Converting product: \(storeProduct.id)")
|
|
93
|
-
return self.convertToNitroProduct(storeProduct, type: type)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
print("[RnIap] Returning \(nitroProducts.count) NitroProducts")
|
|
97
|
-
return nitroProducts
|
|
180
|
+
// Prefer OpenIAP for fetching
|
|
181
|
+
let req = OpenIapProductRequest(skus: skus, type: type)
|
|
182
|
+
let products = try await OpenIapModule.shared.fetchProducts(req)
|
|
183
|
+
return products.map { self.convertOpenIapProductToNitroProduct($0) }
|
|
98
184
|
} catch {
|
|
99
|
-
|
|
100
|
-
let errorJson = ErrorUtils.createErrorJson(from: error)
|
|
101
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
185
|
+
throw OpenIapFailure.storeKitError(error: error)
|
|
102
186
|
}
|
|
103
187
|
}
|
|
104
188
|
}
|
|
105
189
|
|
|
106
190
|
func requestPurchase(request: NitroPurchaseRequest) throws -> Promise<Void> {
|
|
107
191
|
return Promise.async {
|
|
108
|
-
// iOS implementation
|
|
109
192
|
guard let iosRequest = request.ios else {
|
|
110
|
-
// No iOS request, send error event
|
|
111
193
|
let error = self.createPurchaseErrorResult(
|
|
112
|
-
code:
|
|
194
|
+
code: OpenIapError.E_USER_ERROR,
|
|
113
195
|
message: "No iOS request provided"
|
|
114
196
|
)
|
|
115
197
|
self.sendPurchaseError(error)
|
|
116
198
|
return
|
|
117
199
|
}
|
|
118
|
-
|
|
119
200
|
do {
|
|
120
201
|
try self.ensureConnection()
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
code: IapErrorCode.notPrepared,
|
|
124
|
-
message: "Product store not initialized",
|
|
125
|
-
productId: iosRequest.sku
|
|
126
|
-
)
|
|
127
|
-
self.sendPurchaseError(error)
|
|
128
|
-
return
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Get product from store or fetch if not cached
|
|
132
|
-
var product: Product? = await productStore.getProduct(productID: iosRequest.sku)
|
|
133
|
-
|
|
134
|
-
if product == nil {
|
|
135
|
-
// Try fetching from StoreKit if not in cache
|
|
136
|
-
let products = try await Product.products(for: [iosRequest.sku])
|
|
137
|
-
guard let fetchedProduct = products.first else {
|
|
138
|
-
let error = self.createPurchaseErrorResult(
|
|
139
|
-
code: IapErrorCode.itemUnavailable,
|
|
140
|
-
message: "Invalid product ID: \(iosRequest.sku)",
|
|
141
|
-
productId: iosRequest.sku
|
|
142
|
-
)
|
|
143
|
-
self.sendPurchaseError(error)
|
|
144
|
-
return
|
|
145
|
-
}
|
|
146
|
-
// Store for future use
|
|
147
|
-
await productStore.addProduct(fetchedProduct)
|
|
148
|
-
product = fetchedProduct
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Purchase the product - this will send events internally
|
|
152
|
-
try await self.purchaseProductWithEvents(
|
|
153
|
-
product!,
|
|
202
|
+
// Delegate purchase to OpenIAP. It emits success/error events which we bridge above.
|
|
203
|
+
let props = OpenIapRequestPurchaseProps(
|
|
154
204
|
sku: iosRequest.sku,
|
|
155
|
-
andDangerouslyFinishTransactionAutomatically: iosRequest.andDangerouslyFinishTransactionAutomatically
|
|
205
|
+
andDangerouslyFinishTransactionAutomatically: iosRequest.andDangerouslyFinishTransactionAutomatically,
|
|
156
206
|
appAccountToken: iosRequest.appAccountToken,
|
|
157
|
-
quantity: iosRequest.quantity,
|
|
158
|
-
withOffer: iosRequest.withOffer
|
|
207
|
+
quantity: iosRequest.quantity != nil ? Int(iosRequest.quantity!) : nil,
|
|
208
|
+
withOffer: iosRequest.withOffer.flatMap { dict in
|
|
209
|
+
guard let id = dict["identifier"],
|
|
210
|
+
let key = dict["keyIdentifier"],
|
|
211
|
+
let nonce = dict["nonce"],
|
|
212
|
+
let sig = dict["signature"],
|
|
213
|
+
let ts = dict["timestamp"] else { return nil }
|
|
214
|
+
return OpenIapDiscountOffer(identifier: id, keyIdentifier: key, nonce: nonce, signature: sig, timestamp: ts)
|
|
215
|
+
}
|
|
159
216
|
)
|
|
217
|
+
_ = try await OpenIapModule.shared.requestPurchase(props)
|
|
160
218
|
} catch {
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
switch nsError.code {
|
|
169
|
-
case 0: // SKError.unknown
|
|
170
|
-
errorCode = IapErrorCode.unknown
|
|
171
|
-
case 1: // SKError.clientInvalid
|
|
172
|
-
errorCode = IapErrorCode.serviceError
|
|
173
|
-
case 2: // SKError.paymentCancelled
|
|
174
|
-
errorCode = IapErrorCode.userCancelled
|
|
175
|
-
errorMessage = "User cancelled the purchase"
|
|
176
|
-
case 3: // SKError.paymentInvalid
|
|
177
|
-
errorCode = IapErrorCode.userError
|
|
178
|
-
case 4: // SKError.paymentNotAllowed
|
|
179
|
-
errorCode = IapErrorCode.userError
|
|
180
|
-
errorMessage = "Payment not allowed"
|
|
181
|
-
case 5: // SKError.storeProductNotAvailable
|
|
182
|
-
errorCode = IapErrorCode.itemUnavailable
|
|
183
|
-
case 6: // SKError.cloudServicePermissionDenied
|
|
184
|
-
errorCode = IapErrorCode.serviceError
|
|
185
|
-
case 7: // SKError.cloudServiceNetworkConnectionFailed
|
|
186
|
-
errorCode = IapErrorCode.networkError
|
|
187
|
-
case 8: // SKError.cloudServiceRevoked
|
|
188
|
-
errorCode = IapErrorCode.serviceError
|
|
189
|
-
default:
|
|
190
|
-
errorCode = IapErrorCode.purchaseError
|
|
191
|
-
}
|
|
192
|
-
case "NSURLErrorDomain":
|
|
193
|
-
errorCode = IapErrorCode.networkError
|
|
194
|
-
errorMessage = "Network error: \(error.localizedDescription)"
|
|
195
|
-
default:
|
|
196
|
-
if error.localizedDescription.lowercased().contains("network") {
|
|
197
|
-
errorCode = IapErrorCode.networkError
|
|
198
|
-
} else if error.localizedDescription.lowercased().contains("cancelled") {
|
|
199
|
-
errorCode = IapErrorCode.userCancelled
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
let error = self.createPurchaseErrorResult(
|
|
204
|
-
code: errorCode,
|
|
205
|
-
message: errorMessage,
|
|
206
|
-
productId: iosRequest.sku
|
|
207
|
-
)
|
|
208
|
-
self.sendPurchaseError(error)
|
|
209
|
-
} else {
|
|
210
|
-
let error = self.createPurchaseErrorResult(
|
|
211
|
-
code: IapErrorCode.purchaseError,
|
|
212
|
-
message: error.localizedDescription,
|
|
213
|
-
productId: iosRequest.sku
|
|
214
|
-
)
|
|
215
|
-
self.sendPurchaseError(error)
|
|
216
|
-
}
|
|
219
|
+
// OpenIAP will emit error event; also forward here for safety
|
|
220
|
+
let err = self.createPurchaseErrorResult(
|
|
221
|
+
code: OpenIapError.E_SERVICE_ERROR,
|
|
222
|
+
message: error.localizedDescription,
|
|
223
|
+
productId: iosRequest.sku
|
|
224
|
+
)
|
|
225
|
+
self.sendPurchaseError(err)
|
|
217
226
|
}
|
|
218
227
|
}
|
|
219
228
|
}
|
|
@@ -221,134 +230,48 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
221
230
|
func getAvailablePurchases(options: NitroAvailablePurchasesOptions?) throws -> Promise<[NitroPurchase]> {
|
|
222
231
|
return Promise.async {
|
|
223
232
|
try self.ensureConnection()
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
// Get all transactions
|
|
232
|
-
for await verification in Transaction.currentEntitlements {
|
|
233
|
-
switch verification {
|
|
234
|
-
case .verified(let transaction):
|
|
235
|
-
// Get JWS representation from verification result
|
|
236
|
-
let jwsRepresentation = verification.jwsRepresentation
|
|
237
|
-
|
|
238
|
-
if onlyIncludeActiveItems {
|
|
239
|
-
// Only include active subscriptions and non-consumables
|
|
240
|
-
if transaction.productType == .nonConsumable ||
|
|
241
|
-
(transaction.productType == .autoRenewable && transaction.revocationDate == nil) {
|
|
242
|
-
if let products = try? await StoreKit.Product.products(for: [transaction.productID]),
|
|
243
|
-
let product = products.first {
|
|
244
|
-
purchases.append(self.convertToNitroPurchase(transaction, product: product, jwsRepresentation: jwsRepresentation))
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
} else {
|
|
248
|
-
// Include all transactions
|
|
249
|
-
if let products = try? await StoreKit.Product.products(for: [transaction.productID]),
|
|
250
|
-
let product = products.first {
|
|
251
|
-
purchases.append(self.convertToNitroPurchase(transaction, product: product, jwsRepresentation: jwsRepresentation))
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
case .unverified:
|
|
255
|
-
continue
|
|
256
|
-
}
|
|
233
|
+
do {
|
|
234
|
+
let onlyActive = options?.ios?.onlyIncludeActiveItemsIOS ?? options?.ios?.onlyIncludeActiveItems ?? false
|
|
235
|
+
let props = OpenIapPurchaseOptions(alsoPublishToEventListenerIOS: false, onlyIncludeActiveItemsIOS: onlyActive)
|
|
236
|
+
let purchases = try await OpenIapModule.shared.getAvailablePurchases(props)
|
|
237
|
+
return purchases.map { self.convertOpenIapPurchaseToNitroPurchase($0) }
|
|
238
|
+
} catch {
|
|
239
|
+
throw OpenIapFailure.networkError
|
|
257
240
|
}
|
|
258
|
-
|
|
259
|
-
return purchases
|
|
260
241
|
}
|
|
261
242
|
}
|
|
262
243
|
|
|
263
244
|
func finishTransaction(params: NitroFinishTransactionParams) throws -> Promise<Variant_Bool_NitroPurchaseResult> {
|
|
264
245
|
return Promise.async {
|
|
265
|
-
|
|
266
|
-
guard let iosParams = params.ios else {
|
|
267
|
-
// No iOS params, return success
|
|
268
|
-
return .first(true)
|
|
269
|
-
}
|
|
270
|
-
|
|
246
|
+
guard let iosParams = params.ios else { return .first(true) }
|
|
271
247
|
try self.ensureConnection()
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
await transaction.finish()
|
|
279
|
-
return .first(true)
|
|
280
|
-
}
|
|
281
|
-
case .unverified:
|
|
282
|
-
continue
|
|
283
|
-
}
|
|
248
|
+
do {
|
|
249
|
+
let ok = try await OpenIapModule.shared.finishTransaction(transactionIdentifier: iosParams.transactionId)
|
|
250
|
+
return .first(ok)
|
|
251
|
+
} catch {
|
|
252
|
+
let tid = iosParams.transactionId
|
|
253
|
+
throw OpenIapFailure.verificationFailed(reason: "Transaction not found: \(tid)")
|
|
284
254
|
}
|
|
285
|
-
|
|
286
|
-
let errorJson = ErrorUtils.createErrorJson(
|
|
287
|
-
code: IapErrorCode.itemUnavailable,
|
|
288
|
-
message: "Transaction not found: \(iosParams.transactionId)"
|
|
289
|
-
)
|
|
290
|
-
throw NSError(domain: "RnIap", code: 0, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
291
255
|
}
|
|
292
256
|
}
|
|
293
257
|
|
|
294
258
|
func validateReceipt(params: NitroReceiptValidationParams) throws -> Promise<Variant_NitroReceiptValidationResultIOS_NitroReceiptValidationResultAndroid> {
|
|
295
259
|
return Promise.async {
|
|
296
260
|
do {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
code: IapErrorCode.receiptFailed,
|
|
302
|
-
message: "App receipt not found or could not be read"
|
|
303
|
-
)
|
|
304
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
261
|
+
let result = try await OpenIapModule.shared.validateReceiptIOS(OpenIapReceiptValidationProps(sku: params.sku))
|
|
262
|
+
var latest: NitroPurchase? = nil
|
|
263
|
+
if let tx = result.latestTransaction {
|
|
264
|
+
latest = self.convertOpenIapPurchaseToNitroPurchase(tx)
|
|
305
265
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
var latestTransaction: NitroPurchase? = nil
|
|
312
|
-
|
|
313
|
-
// Find the latest transaction for the specified SKU
|
|
314
|
-
for await verificationResult in Transaction.currentEntitlements {
|
|
315
|
-
switch verificationResult {
|
|
316
|
-
case .verified(let transaction):
|
|
317
|
-
if transaction.productID == params.sku {
|
|
318
|
-
// Fetch the product details for this transaction
|
|
319
|
-
if let products = try? await StoreKit.Product.products(for: [transaction.productID]),
|
|
320
|
-
let product = products.first {
|
|
321
|
-
latestTransaction = self.convertToNitroPurchase(transaction, product: product, jwsRepresentation: nil)
|
|
322
|
-
}
|
|
323
|
-
break
|
|
324
|
-
}
|
|
325
|
-
case .unverified(_, let verificationError):
|
|
326
|
-
// Handle unverified transactions if needed
|
|
327
|
-
print("Unverified transaction for SKU \(params.sku): \(verificationError)")
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// For StoreKit 2, the receipt is always considered valid if we can read it
|
|
332
|
-
// and the transaction verification passed
|
|
333
|
-
let isValid = latestTransaction != nil
|
|
334
|
-
|
|
335
|
-
// Generate JWS representation (simplified for now)
|
|
336
|
-
let jwsRepresentation = receiptDataBase64 // In a real implementation, this would be the actual JWS
|
|
337
|
-
|
|
338
|
-
let result = NitroReceiptValidationResultIOS(
|
|
339
|
-
isValid: isValid,
|
|
340
|
-
receiptData: receiptDataBase64,
|
|
341
|
-
jwsRepresentation: jwsRepresentation,
|
|
342
|
-
latestTransaction: latestTransaction
|
|
266
|
+
let mapped = NitroReceiptValidationResultIOS(
|
|
267
|
+
isValid: result.isValid,
|
|
268
|
+
receiptData: result.receiptData,
|
|
269
|
+
jwsRepresentation: result.jwsRepresentation,
|
|
270
|
+
latestTransaction: latest
|
|
343
271
|
)
|
|
344
|
-
return
|
|
345
|
-
|
|
272
|
+
return .first(mapped)
|
|
346
273
|
} catch {
|
|
347
|
-
|
|
348
|
-
code: IapErrorCode.receiptFailed,
|
|
349
|
-
message: "Receipt validation failed: \(error.localizedDescription)"
|
|
350
|
-
)
|
|
351
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
274
|
+
throw OpenIapFailure.invalidReceipt
|
|
352
275
|
}
|
|
353
276
|
}
|
|
354
277
|
}
|
|
@@ -357,146 +280,96 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
357
280
|
|
|
358
281
|
func getStorefrontIOS() throws -> Promise<String> {
|
|
359
282
|
return Promise.async {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
} else {
|
|
365
|
-
// If no storefront is available, throw an error
|
|
366
|
-
let errorJson = ErrorUtils.createErrorJson(
|
|
367
|
-
code: IapErrorCode.unknown,
|
|
368
|
-
message: "Unable to retrieve storefront information"
|
|
369
|
-
)
|
|
370
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
283
|
+
do {
|
|
284
|
+
return try await OpenIapModule.shared.getStorefrontIOS()
|
|
285
|
+
} catch {
|
|
286
|
+
throw OpenIapFailure.storeKitError(error: error)
|
|
371
287
|
}
|
|
372
288
|
}
|
|
373
289
|
}
|
|
374
290
|
|
|
375
291
|
func getAppTransactionIOS() throws -> Promise<String?> {
|
|
376
292
|
return Promise.async {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
293
|
+
do {
|
|
294
|
+
if #available(iOS 16.0, *) {
|
|
295
|
+
if let appTx = try await OpenIapModule.shared.getAppTransactionIOS() {
|
|
296
|
+
var result: [String: Any?] = [
|
|
297
|
+
"bundleId": appTx.bundleId,
|
|
298
|
+
"appVersion": appTx.appVersion,
|
|
299
|
+
"originalAppVersion": appTx.originalAppVersion,
|
|
300
|
+
"originalPurchaseDate": appTx.originalPurchaseDate.timeIntervalSince1970 * 1000,
|
|
301
|
+
"deviceVerification": appTx.deviceVerification,
|
|
302
|
+
"deviceVerificationNonce": appTx.deviceVerificationNonce,
|
|
303
|
+
"environment": appTx.environment,
|
|
304
|
+
"signedDate": appTx.signedDate.timeIntervalSince1970 * 1000,
|
|
305
|
+
"appId": appTx.appId,
|
|
306
|
+
"appVersionId": appTx.appVersionId,
|
|
307
|
+
"preorderDate": appTx.preorderDate != nil ? (appTx.preorderDate!.timeIntervalSince1970 * 1000) : nil
|
|
308
|
+
]
|
|
309
|
+
result["appTransactionId"] = appTx.appTransactionId
|
|
310
|
+
result["originalPlatform"] = appTx.originalPlatform
|
|
311
|
+
let jsonData = try JSONSerialization.data(withJSONObject: result, options: [])
|
|
312
|
+
return String(data: jsonData, encoding: .utf8)
|
|
313
|
+
}
|
|
314
|
+
return nil
|
|
315
|
+
} else {
|
|
386
316
|
return nil
|
|
387
317
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
"bundleId": appTransaction.bundleID,
|
|
391
|
-
"appVersion": appTransaction.appVersion,
|
|
392
|
-
"originalAppVersion": appTransaction.originalAppVersion,
|
|
393
|
-
"originalPurchaseDate": appTransaction.originalPurchaseDate.timeIntervalSince1970 * 1000,
|
|
394
|
-
"deviceVerification": appTransaction.deviceVerification.base64EncodedString(),
|
|
395
|
-
"deviceVerificationNonce": appTransaction.deviceVerificationNonce.uuidString,
|
|
396
|
-
"environment": appTransaction.environment.rawValue,
|
|
397
|
-
"signedDate": appTransaction.signedDate.timeIntervalSince1970 * 1000,
|
|
398
|
-
"appId": appTransaction.appID,
|
|
399
|
-
"appVersionId": appTransaction.appVersionID,
|
|
400
|
-
"preorderDate": appTransaction.preorderDate.map { $0.timeIntervalSince1970 * 1000 }
|
|
401
|
-
]
|
|
402
|
-
|
|
403
|
-
// iOS 18.4+ properties - only compile with Xcode 16.4+ (Swift 6.1+)
|
|
404
|
-
// This prevents build failures on Xcode 16.3 and below
|
|
405
|
-
#if swift(>=6.1)
|
|
406
|
-
if #available(iOS 18.4, *) {
|
|
407
|
-
result["appTransactionId"] = appTransaction.appTransactionID
|
|
408
|
-
result["originalPlatform"] = appTransaction.originalPlatform.rawValue
|
|
409
|
-
}
|
|
410
|
-
#endif
|
|
411
|
-
|
|
412
|
-
// Convert dictionary to JSON string
|
|
413
|
-
let jsonData = try JSONSerialization.data(withJSONObject: result, options: [])
|
|
414
|
-
return String(data: jsonData, encoding: .utf8)
|
|
415
|
-
#else
|
|
416
|
-
let errorJson = ErrorUtils.createErrorJson(
|
|
417
|
-
code: IapErrorCode.unknown,
|
|
418
|
-
message: "getAppTransaction requires Xcode 15.0+ with iOS 16.0 SDK for compilation"
|
|
419
|
-
)
|
|
420
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
421
|
-
#endif
|
|
422
|
-
} else {
|
|
423
|
-
let errorJson = ErrorUtils.createErrorJson(
|
|
424
|
-
code: IapErrorCode.unknown,
|
|
425
|
-
message: "getAppTransaction requires iOS 16.0 or later"
|
|
426
|
-
)
|
|
427
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
318
|
+
} catch {
|
|
319
|
+
return nil
|
|
428
320
|
}
|
|
429
321
|
}
|
|
430
322
|
}
|
|
431
323
|
|
|
432
324
|
func requestPromotedProductIOS() throws -> Promise<NitroProduct?> {
|
|
433
325
|
return Promise.async {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
326
|
+
do {
|
|
327
|
+
if let p = try await OpenIapModule.shared.getPromotedProductIOS() {
|
|
328
|
+
var n = NitroProduct()
|
|
329
|
+
n.id = p.productIdentifier
|
|
330
|
+
n.title = p.localizedTitle
|
|
331
|
+
n.description = p.localizedDescription
|
|
332
|
+
n.type = "inapp"
|
|
333
|
+
n.platform = "ios"
|
|
334
|
+
n.price = p.price
|
|
335
|
+
n.currency = p.priceLocale.currencyCode
|
|
336
|
+
return n
|
|
444
337
|
}
|
|
338
|
+
return nil
|
|
339
|
+
} catch {
|
|
340
|
+
return nil
|
|
445
341
|
}
|
|
446
|
-
return nil
|
|
447
342
|
}
|
|
448
343
|
}
|
|
449
344
|
|
|
450
345
|
func buyPromotedProductIOS() throws -> Promise<Void> {
|
|
451
346
|
return Promise.async {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
message: "No promoted product available"
|
|
457
|
-
)
|
|
458
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
347
|
+
do {
|
|
348
|
+
try await OpenIapModule.shared.requestPurchaseOnPromotedProductIOS()
|
|
349
|
+
} catch {
|
|
350
|
+
throw OpenIapFailure.productNotFound(id: "promoted-product")
|
|
459
351
|
}
|
|
460
|
-
|
|
461
|
-
// Add the deferred payment to the queue
|
|
462
|
-
SKPaymentQueue.default().add(payment)
|
|
463
|
-
|
|
464
|
-
// Clear the promoted product data
|
|
465
|
-
self.promotedPayment = nil
|
|
466
|
-
self.promotedProduct = nil
|
|
467
352
|
}
|
|
468
353
|
}
|
|
469
354
|
|
|
470
355
|
func presentCodeRedemptionSheetIOS() throws -> Promise<Bool> {
|
|
471
356
|
return Promise.async {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
357
|
+
do {
|
|
358
|
+
let ok = try await OpenIapModule.shared.presentCodeRedemptionSheetIOS()
|
|
359
|
+
return ok
|
|
360
|
+
} catch {
|
|
361
|
+
// Fallback with explicit error for simulator or unsupported cases
|
|
362
|
+
throw OpenIapFailure.notSupported
|
|
476
363
|
}
|
|
477
|
-
return true
|
|
478
|
-
#else
|
|
479
|
-
// Not available on simulator
|
|
480
|
-
let errorJson = ErrorUtils.createErrorJson(
|
|
481
|
-
code: IapErrorCode.itemUnavailable,
|
|
482
|
-
message: "Code redemption sheet is not available on simulator"
|
|
483
|
-
)
|
|
484
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
485
|
-
#endif
|
|
486
364
|
}
|
|
487
365
|
}
|
|
488
366
|
|
|
489
367
|
func clearTransactionIOS() throws -> Promise<Void> {
|
|
490
368
|
return Promise.async {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
await transaction.finish()
|
|
496
|
-
self.transactions.removeValue(forKey: String(transaction.id))
|
|
497
|
-
} catch {
|
|
498
|
-
print("Failed to finish transaction: \(error.localizedDescription)")
|
|
499
|
-
}
|
|
369
|
+
do {
|
|
370
|
+
try await OpenIapModule.shared.clearTransactionIOS()
|
|
371
|
+
} catch {
|
|
372
|
+
// ignore
|
|
500
373
|
}
|
|
501
374
|
}
|
|
502
375
|
}
|
|
@@ -506,39 +379,37 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
506
379
|
func subscriptionStatusIOS(sku: String) throws -> Promise<[NitroSubscriptionStatus]?> {
|
|
507
380
|
return Promise.async {
|
|
508
381
|
try self.ensureConnection()
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
382
|
+
do {
|
|
383
|
+
if let statuses = try await OpenIapModule.shared.subscriptionStatusIOS(sku: sku) {
|
|
384
|
+
return statuses.map { s in
|
|
385
|
+
var renewal: NitroSubscriptionRenewalInfo? = nil
|
|
386
|
+
if let r = s.renewalInfo {
|
|
387
|
+
renewal = NitroSubscriptionRenewalInfo(
|
|
388
|
+
autoRenewStatus: r.autoRenewStatus,
|
|
389
|
+
autoRenewPreference: r.autoRenewPreference,
|
|
390
|
+
expirationReason: r.expirationReason.map { Double($0) },
|
|
391
|
+
gracePeriodExpirationDate: r.gracePeriodExpirationDate?.timeIntervalSince1970,
|
|
392
|
+
currentProductID: r.currentProductID,
|
|
393
|
+
platform: "ios"
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
let isActive: Bool
|
|
397
|
+
switch s.state {
|
|
398
|
+
case .subscribed:
|
|
399
|
+
isActive = true
|
|
400
|
+
default:
|
|
401
|
+
isActive = false
|
|
402
|
+
}
|
|
403
|
+
return NitroSubscriptionStatus(
|
|
404
|
+
state: isActive ? 1 : 0,
|
|
405
|
+
platform: "ios",
|
|
406
|
+
renewalInfo: renewal
|
|
407
|
+
)
|
|
408
|
+
}
|
|
535
409
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
platform: "ios",
|
|
540
|
-
renewalInfo: renewalInfo
|
|
541
|
-
)
|
|
410
|
+
return []
|
|
411
|
+
} catch {
|
|
412
|
+
return []
|
|
542
413
|
}
|
|
543
414
|
}
|
|
544
415
|
}
|
|
@@ -546,43 +417,13 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
546
417
|
func currentEntitlementIOS(sku: String) throws -> Promise<NitroPurchase?> {
|
|
547
418
|
return Promise.async {
|
|
548
419
|
try self.ensureConnection()
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
let product = products.first else {
|
|
553
|
-
let errorJson = ErrorUtils.createErrorJson(
|
|
554
|
-
code: IapErrorCode.itemUnavailable,
|
|
555
|
-
message: "Can't find product for sku \(sku)"
|
|
556
|
-
)
|
|
557
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
// Get current entitlement
|
|
561
|
-
guard let entitlement = await product.currentEntitlement else {
|
|
562
|
-
let errorJson = ErrorUtils.createErrorJson(
|
|
563
|
-
code: IapErrorCode.itemUnavailable,
|
|
564
|
-
message: "Can't find entitlement for sku \(sku)"
|
|
565
|
-
)
|
|
566
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
switch entitlement {
|
|
570
|
-
case .verified(let transaction):
|
|
571
|
-
// Get renewal info if this is a subscription
|
|
572
|
-
var renewalInfo: Product.SubscriptionInfo.RenewalInfo? = nil
|
|
573
|
-
if let subscription = product.subscription,
|
|
574
|
-
let status = try? await subscription.status.first {
|
|
575
|
-
if case .verified(let info) = status.renewalInfo {
|
|
576
|
-
renewalInfo = info
|
|
577
|
-
}
|
|
420
|
+
do {
|
|
421
|
+
if let p = try await OpenIapModule.shared.currentEntitlementIOS(sku: sku) {
|
|
422
|
+
return self.convertOpenIapPurchaseToNitroPurchase(p)
|
|
578
423
|
}
|
|
579
|
-
return
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
code: IapErrorCode.transactionValidationFailed,
|
|
583
|
-
message: "Failed to verify transaction for sku \(sku)"
|
|
584
|
-
)
|
|
585
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
424
|
+
return nil
|
|
425
|
+
} catch {
|
|
426
|
+
throw OpenIapFailure.productNotFound(id: sku)
|
|
586
427
|
}
|
|
587
428
|
}
|
|
588
429
|
}
|
|
@@ -590,206 +431,68 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
590
431
|
func latestTransactionIOS(sku: String) throws -> Promise<NitroPurchase?> {
|
|
591
432
|
return Promise.async {
|
|
592
433
|
try self.ensureConnection()
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
let product = products.first else {
|
|
597
|
-
let errorJson = ErrorUtils.createErrorJson(
|
|
598
|
-
code: IapErrorCode.itemUnavailable,
|
|
599
|
-
message: "Can't find product for sku \(sku)"
|
|
600
|
-
)
|
|
601
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// Get latest transaction
|
|
605
|
-
guard let latestTransaction = await product.latestTransaction else {
|
|
606
|
-
let errorJson = ErrorUtils.createErrorJson(
|
|
607
|
-
code: IapErrorCode.itemUnavailable,
|
|
608
|
-
message: "Can't find latest transaction for sku \(sku)"
|
|
609
|
-
)
|
|
610
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
switch latestTransaction {
|
|
614
|
-
case .verified(let transaction):
|
|
615
|
-
// Get renewal info if this is a subscription
|
|
616
|
-
var renewalInfo: Product.SubscriptionInfo.RenewalInfo? = nil
|
|
617
|
-
if let subscription = product.subscription,
|
|
618
|
-
let status = try? await subscription.status.first {
|
|
619
|
-
if case .verified(let info) = status.renewalInfo {
|
|
620
|
-
renewalInfo = info
|
|
621
|
-
}
|
|
434
|
+
do {
|
|
435
|
+
if let p = try await OpenIapModule.shared.latestTransactionIOS(sku: sku) {
|
|
436
|
+
return self.convertOpenIapPurchaseToNitroPurchase(p)
|
|
622
437
|
}
|
|
623
|
-
return
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
code: IapErrorCode.transactionValidationFailed,
|
|
627
|
-
message: "Failed to verify transaction for sku \(sku)"
|
|
628
|
-
)
|
|
629
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
438
|
+
return nil
|
|
439
|
+
} catch {
|
|
440
|
+
throw OpenIapFailure.productNotFound(id: sku)
|
|
630
441
|
}
|
|
631
442
|
}
|
|
632
443
|
}
|
|
633
444
|
|
|
634
445
|
func getPendingTransactionsIOS() throws -> Promise<[NitroPurchase]> {
|
|
635
446
|
return Promise.async {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
if let products = try? await StoreKit.Product.products(for: [transaction.productID]),
|
|
642
|
-
let product = products.first {
|
|
643
|
-
pendingPurchases.append(self.convertToNitroPurchase(transaction, product: product))
|
|
644
|
-
}
|
|
447
|
+
do {
|
|
448
|
+
let pending = try await OpenIapModule.shared.getPendingTransactionsIOS()
|
|
449
|
+
return pending.map { self.convertOpenIapPurchaseToNitroPurchase($0) }
|
|
450
|
+
} catch {
|
|
451
|
+
return []
|
|
645
452
|
}
|
|
646
|
-
|
|
647
|
-
return pendingPurchases
|
|
648
453
|
}
|
|
649
454
|
}
|
|
650
455
|
|
|
651
456
|
func syncIOS() throws -> Promise<Bool> {
|
|
652
457
|
return Promise.async {
|
|
653
458
|
do {
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
return true
|
|
459
|
+
let ok = try await OpenIapModule.shared.syncIOS()
|
|
460
|
+
return ok
|
|
657
461
|
} catch {
|
|
658
|
-
|
|
659
|
-
code: IapErrorCode.syncError,
|
|
660
|
-
message: "Error synchronizing with the AppStore: \(error.localizedDescription)"
|
|
661
|
-
)
|
|
662
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
462
|
+
throw OpenIapFailure.networkError
|
|
663
463
|
}
|
|
664
464
|
}
|
|
665
465
|
}
|
|
666
466
|
|
|
667
467
|
func showManageSubscriptionsIOS() throws -> Promise<[NitroPurchase]> {
|
|
668
468
|
return Promise.async {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
code: IapErrorCode.serviceError,
|
|
678
|
-
message: "Cannot find active window scene"
|
|
679
|
-
)
|
|
680
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
// Get current subscription statuses before showing UI
|
|
684
|
-
var beforeStatuses: [String: Bool] = [:]
|
|
685
|
-
var subscriptionSkus = await self.productStore?.getAllSubscriptionProductIds() ?? []
|
|
686
|
-
|
|
687
|
-
// Fallback: If ProductStore is empty, derive SKUs from current entitlements
|
|
688
|
-
if subscriptionSkus.isEmpty {
|
|
689
|
-
var ids = Set<String>()
|
|
690
|
-
for await verification in Transaction.currentEntitlements {
|
|
691
|
-
if case .verified(let t) = verification, t.productType == .autoRenewable {
|
|
692
|
-
ids.insert(t.productID)
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
subscriptionSkus = Array(ids)
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
for sku in subscriptionSkus {
|
|
699
|
-
if let product = await self.fetchProductIfNeeded(sku: sku),
|
|
700
|
-
let status = try? await product.subscription?.status.first {
|
|
701
|
-
var willAutoRenew = false
|
|
702
|
-
if case .verified(let info) = status.renewalInfo {
|
|
703
|
-
willAutoRenew = info.willAutoRenew
|
|
704
|
-
}
|
|
705
|
-
beforeStatuses[sku] = willAutoRenew
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// Show the management UI
|
|
710
|
-
try await AppStore.showManageSubscriptions(in: windowScene)
|
|
711
|
-
|
|
712
|
-
// Wait a bit for changes to propagate
|
|
713
|
-
try? await Task.sleep(nanoseconds: 1_500_000_000) // 1.5 seconds
|
|
714
|
-
|
|
715
|
-
// Check for changes and return updated subscriptions
|
|
716
|
-
var updatedSubscriptions: [NitroPurchase] = []
|
|
717
|
-
|
|
718
|
-
for sku in subscriptionSkus {
|
|
719
|
-
if let product = await self.fetchProductIfNeeded(sku: sku),
|
|
720
|
-
let status = try? await product.subscription?.status.first,
|
|
721
|
-
let result = await product.latestTransaction {
|
|
722
|
-
|
|
723
|
-
// Check current status
|
|
724
|
-
var currentWillAutoRenew = false
|
|
725
|
-
var currentRenewalInfo: Product.SubscriptionInfo.RenewalInfo? = nil
|
|
726
|
-
if case .verified(let info) = status.renewalInfo {
|
|
727
|
-
currentWillAutoRenew = info.willAutoRenew
|
|
728
|
-
currentRenewalInfo = info
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
// Check if status changed
|
|
732
|
-
let previousWillAutoRenew = beforeStatuses[sku] ?? false
|
|
733
|
-
if previousWillAutoRenew != currentWillAutoRenew {
|
|
734
|
-
// Status changed, include in result
|
|
735
|
-
do {
|
|
736
|
-
let transaction = try self.checkVerified(result)
|
|
737
|
-
let purchase = self.convertToNitroPurchase(
|
|
738
|
-
transaction,
|
|
739
|
-
product: product,
|
|
740
|
-
jwsRepresentation: result.jwsRepresentation,
|
|
741
|
-
renewalInfo: currentRenewalInfo
|
|
742
|
-
)
|
|
743
|
-
|
|
744
|
-
// Add renewal info as additional data
|
|
745
|
-
// Note: We'll add this info through the purchase token or other field
|
|
746
|
-
// since NitroPurchase doesn't have a dedicated renewal info field
|
|
747
|
-
|
|
748
|
-
updatedSubscriptions.append(purchase)
|
|
749
|
-
} catch {
|
|
750
|
-
// Skip if verification fails
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
}
|
|
469
|
+
do {
|
|
470
|
+
// Trigger system UI
|
|
471
|
+
_ = try await OpenIapModule.shared.showManageSubscriptionsIOS()
|
|
472
|
+
// Return current entitlements as approximation of updates
|
|
473
|
+
let purchases = try await OpenIapModule.shared.getAvailablePurchases(OpenIapPurchaseOptions(alsoPublishToEventListenerIOS: false, onlyIncludeActiveItemsIOS: true))
|
|
474
|
+
return purchases.map { self.convertOpenIapPurchaseToNitroPurchase($0) }
|
|
475
|
+
} catch {
|
|
476
|
+
throw OpenIapFailure.storeKitError(error: error)
|
|
754
477
|
}
|
|
755
|
-
|
|
756
|
-
return updatedSubscriptions
|
|
757
|
-
#else
|
|
758
|
-
let errorJson = ErrorUtils.createErrorJson(
|
|
759
|
-
code: IapErrorCode.serviceError,
|
|
760
|
-
message: "This method is not available on tvOS"
|
|
761
|
-
)
|
|
762
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
763
|
-
#endif
|
|
764
478
|
}
|
|
765
479
|
}
|
|
766
480
|
|
|
767
481
|
func isEligibleForIntroOfferIOS(groupID: String) throws -> Promise<Bool> {
|
|
768
482
|
return Promise.async {
|
|
769
|
-
return await
|
|
483
|
+
return await OpenIapModule.shared.isEligibleForIntroOfferIOS(groupID: groupID)
|
|
770
484
|
}
|
|
771
485
|
}
|
|
772
486
|
|
|
773
487
|
func getReceiptDataIOS() throws -> Promise<String> {
|
|
774
488
|
return Promise.async {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
|
|
779
|
-
return receiptData.base64EncodedString(options: [])
|
|
780
|
-
} catch {
|
|
781
|
-
let errorJson = ErrorUtils.createErrorJson(
|
|
782
|
-
code: IapErrorCode.receiptFailed,
|
|
783
|
-
message: "Error reading receipt data: \(error.localizedDescription)"
|
|
784
|
-
)
|
|
785
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
489
|
+
do {
|
|
490
|
+
if let receipt = try await OpenIapModule.shared.getReceiptDataIOS() {
|
|
491
|
+
return receipt
|
|
786
492
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
message: "App Store receipt not found"
|
|
791
|
-
)
|
|
792
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
493
|
+
throw OpenIapFailure.invalidReceipt
|
|
494
|
+
} catch {
|
|
495
|
+
throw OpenIapFailure.invalidReceipt
|
|
793
496
|
}
|
|
794
497
|
}
|
|
795
498
|
}
|
|
@@ -797,129 +500,48 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
797
500
|
func isTransactionVerifiedIOS(sku: String) throws -> Promise<Bool> {
|
|
798
501
|
return Promise.async {
|
|
799
502
|
try self.ensureConnection()
|
|
800
|
-
|
|
801
|
-
// Get the product from the store
|
|
802
|
-
guard let products = try? await StoreKit.Product.products(for: [sku]),
|
|
803
|
-
let product = products.first,
|
|
804
|
-
let result = await product.latestTransaction else {
|
|
805
|
-
return false
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
// Check if transaction is verified
|
|
809
|
-
switch result {
|
|
810
|
-
case .verified:
|
|
811
|
-
return true
|
|
812
|
-
case .unverified:
|
|
813
|
-
return false
|
|
814
|
-
}
|
|
503
|
+
return await OpenIapModule.shared.isTransactionVerifiedIOS(sku: sku)
|
|
815
504
|
}
|
|
816
505
|
}
|
|
817
506
|
|
|
818
507
|
func getTransactionJwsIOS(sku: String) throws -> Promise<String?> {
|
|
819
508
|
return Promise.async {
|
|
820
509
|
try self.ensureConnection()
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
guard let products = try? await StoreKit.Product.products(for: [sku]),
|
|
824
|
-
let product = products.first,
|
|
825
|
-
let result = await product.latestTransaction else {
|
|
826
|
-
let errorJson = ErrorUtils.createErrorJson(
|
|
827
|
-
code: IapErrorCode.itemUnavailable,
|
|
828
|
-
message: "Can't find transaction for sku \(sku)"
|
|
829
|
-
)
|
|
830
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
510
|
+
do { return try await OpenIapModule.shared.getTransactionJwsIOS(sku: sku) } catch {
|
|
511
|
+
throw OpenIapFailure.verificationFailed(reason: "Can't find transaction for sku \(sku)")
|
|
831
512
|
}
|
|
832
|
-
|
|
833
|
-
return result.jwsRepresentation
|
|
834
513
|
}
|
|
835
514
|
}
|
|
836
515
|
|
|
837
516
|
func beginRefundRequestIOS(sku: String) throws -> Promise<String?> {
|
|
838
517
|
return Promise.async {
|
|
839
|
-
|
|
840
|
-
if #available(macOS 12.0, *) {
|
|
841
|
-
// Find the latest transaction for the SKU
|
|
842
|
-
var latestTransaction: Transaction? = nil
|
|
843
|
-
|
|
844
|
-
for await result in Transaction.currentEntitlements {
|
|
845
|
-
switch result {
|
|
846
|
-
case .verified(let transaction):
|
|
847
|
-
if transaction.productID == sku {
|
|
848
|
-
latestTransaction = transaction
|
|
849
|
-
break
|
|
850
|
-
}
|
|
851
|
-
case .unverified(_, _):
|
|
852
|
-
continue
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
guard let transaction = latestTransaction else {
|
|
857
|
-
let errorJson = ErrorUtils.createErrorJson(
|
|
858
|
-
code: IapErrorCode.itemUnavailable,
|
|
859
|
-
message: "Can't find transaction for SKU \(sku)"
|
|
860
|
-
)
|
|
861
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
// Begin refund request
|
|
865
|
-
do {
|
|
866
|
-
// Get the active window scene
|
|
867
|
-
guard let windowScene = await MainActor.run(body: {
|
|
868
|
-
UIApplication.shared.connectedScenes
|
|
869
|
-
.compactMap({ $0 as? UIWindowScene })
|
|
870
|
-
.first(where: { $0.activationState == .foregroundActive })
|
|
871
|
-
}) else {
|
|
872
|
-
let errorJson = ErrorUtils.createErrorJson(
|
|
873
|
-
code: IapErrorCode.serviceError,
|
|
874
|
-
message: "Cannot find active window scene"
|
|
875
|
-
)
|
|
876
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
let refundStatus = try await transaction.beginRefundRequest(in: windowScene)
|
|
880
|
-
|
|
881
|
-
// Convert refund status to string
|
|
882
|
-
switch refundStatus {
|
|
883
|
-
case .success:
|
|
884
|
-
return "success"
|
|
885
|
-
case .userCancelled:
|
|
886
|
-
return "userCancelled"
|
|
887
|
-
@unknown default:
|
|
888
|
-
return "unknown"
|
|
889
|
-
}
|
|
890
|
-
} catch {
|
|
891
|
-
let errorJson = ErrorUtils.createErrorJson(
|
|
892
|
-
code: IapErrorCode.serviceError,
|
|
893
|
-
message: "Failed to begin refund request: \(error.localizedDescription)"
|
|
894
|
-
)
|
|
895
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
896
|
-
}
|
|
897
|
-
} else {
|
|
898
|
-
// Refund request is only available on iOS 15+
|
|
899
|
-
let errorJson = ErrorUtils.createErrorJson(
|
|
900
|
-
code: IapErrorCode.itemUnavailable,
|
|
901
|
-
message: "Refund request requires iOS 15.0 or later"
|
|
902
|
-
)
|
|
903
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
904
|
-
}
|
|
905
|
-
#else
|
|
906
|
-
// Not available on tvOS
|
|
907
|
-
let errorJson = ErrorUtils.createErrorJson(
|
|
908
|
-
code: IapErrorCode.itemUnavailable,
|
|
909
|
-
message: "Refund request is not available on tvOS"
|
|
910
|
-
)
|
|
911
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
912
|
-
#endif
|
|
518
|
+
do { return try await OpenIapModule.shared.beginRefundRequestIOS(sku: sku) } catch { return nil }
|
|
913
519
|
}
|
|
914
520
|
}
|
|
915
521
|
|
|
916
522
|
func addPromotedProductListenerIOS(listener: @escaping (NitroProduct) -> Void) throws {
|
|
917
523
|
promotedProductListeners.append(listener)
|
|
918
524
|
|
|
919
|
-
// If
|
|
920
|
-
|
|
921
|
-
let
|
|
922
|
-
|
|
525
|
+
// If a promoted product is already available from OpenIAP, notify immediately
|
|
526
|
+
Task {
|
|
527
|
+
if let p = try? await OpenIapModule.shared.getPromotedProductIOS() {
|
|
528
|
+
let id = p.productIdentifier
|
|
529
|
+
let title = p.localizedTitle
|
|
530
|
+
let desc = p.localizedDescription
|
|
531
|
+
let price = p.price
|
|
532
|
+
let currency = p.priceLocale.currencyCode
|
|
533
|
+
await MainActor.run {
|
|
534
|
+
var n = NitroProduct()
|
|
535
|
+
n.id = id
|
|
536
|
+
n.title = title
|
|
537
|
+
n.description = desc
|
|
538
|
+
n.type = "inapp"
|
|
539
|
+
n.platform = "ios"
|
|
540
|
+
n.price = price
|
|
541
|
+
n.currency = currency
|
|
542
|
+
listener(n)
|
|
543
|
+
}
|
|
544
|
+
}
|
|
923
545
|
}
|
|
924
546
|
}
|
|
925
547
|
|
|
@@ -953,443 +575,10 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
953
575
|
|
|
954
576
|
// MARK: - Private Helper Methods
|
|
955
577
|
|
|
956
|
-
private func fetchProductIfNeeded(sku: String) async -> Product? {
|
|
957
|
-
// Check if product is already cached
|
|
958
|
-
if let product = await self.productStore?.getProduct(productID: sku) {
|
|
959
|
-
return product
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
// Fetch from StoreKit and cache it
|
|
963
|
-
if let products = try? await StoreKit.Product.products(for: [sku]),
|
|
964
|
-
let product = products.first {
|
|
965
|
-
await self.productStore?.addProduct(product)
|
|
966
|
-
return product
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
return nil
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
private func purchaseProduct(
|
|
973
|
-
_ product: Product,
|
|
974
|
-
sku: String,
|
|
975
|
-
andDangerouslyFinishTransactionAutomatically: Bool,
|
|
976
|
-
appAccountToken: String?,
|
|
977
|
-
quantity: Double?,
|
|
978
|
-
withOffer: [String: String]?
|
|
979
|
-
) async throws -> NitroPurchase {
|
|
980
|
-
// Prepare purchase options
|
|
981
|
-
var options: Set<Product.PurchaseOption> = []
|
|
982
|
-
|
|
983
|
-
// Add quantity if specified
|
|
984
|
-
if let quantity = quantity, quantity > 0 {
|
|
985
|
-
options.insert(.quantity(Int(quantity)))
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
// Add promotional offer if provided
|
|
989
|
-
if let offerID = withOffer?["identifier"],
|
|
990
|
-
let keyID = withOffer?["keyIdentifier"],
|
|
991
|
-
let nonce = withOffer?["nonce"],
|
|
992
|
-
let signature = withOffer?["signature"],
|
|
993
|
-
let timestamp = withOffer?["timestamp"],
|
|
994
|
-
let uuidNonce = UUID(uuidString: nonce),
|
|
995
|
-
let signatureData = Data(base64Encoded: signature),
|
|
996
|
-
let timestampInt = Int(timestamp) {
|
|
997
|
-
options.insert(
|
|
998
|
-
.promotionalOffer(
|
|
999
|
-
offerID: offerID,
|
|
1000
|
-
keyID: keyID,
|
|
1001
|
-
nonce: uuidNonce,
|
|
1002
|
-
signature: signatureData,
|
|
1003
|
-
timestamp: timestampInt
|
|
1004
|
-
)
|
|
1005
|
-
)
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
// Add app account token if provided
|
|
1009
|
-
if let appAccountToken = appAccountToken,
|
|
1010
|
-
let appAccountUUID = UUID(uuidString: appAccountToken) {
|
|
1011
|
-
options.insert(.appAccountToken(appAccountUUID))
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// Get window scene for iOS 17+ purchase confirmation
|
|
1015
|
-
let windowScene = await currentWindowScene()
|
|
1016
|
-
|
|
1017
|
-
// Perform the purchase
|
|
1018
|
-
let result: Product.PurchaseResult
|
|
1019
|
-
#if swift(>=5.9)
|
|
1020
|
-
if #available(iOS 17.0, tvOS 17.0, *) {
|
|
1021
|
-
if let windowScene = windowScene {
|
|
1022
|
-
result = try await product.purchase(confirmIn: windowScene, options: options)
|
|
1023
|
-
} else {
|
|
1024
|
-
result = try await product.purchase(options: options)
|
|
1025
|
-
}
|
|
1026
|
-
} else {
|
|
1027
|
-
#if !os(visionOS)
|
|
1028
|
-
result = try await product.purchase(options: options)
|
|
1029
|
-
#endif
|
|
1030
|
-
}
|
|
1031
|
-
#elseif !os(visionOS)
|
|
1032
|
-
result = try await product.purchase(options: options)
|
|
1033
|
-
#endif
|
|
1034
|
-
|
|
1035
|
-
switch result {
|
|
1036
|
-
case .success(let verification):
|
|
1037
|
-
let transaction = try checkVerified(verification)
|
|
1038
|
-
|
|
1039
|
-
// Store transaction if not auto-finishing
|
|
1040
|
-
if !andDangerouslyFinishTransactionAutomatically {
|
|
1041
|
-
self.transactions[String(transaction.id)] = transaction
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
// Get JWS representation
|
|
1045
|
-
let jwsRepresentation = verification.jwsRepresentation
|
|
1046
|
-
|
|
1047
|
-
// Create purchase object
|
|
1048
|
-
let purchase = self.convertToNitroPurchase(
|
|
1049
|
-
transaction,
|
|
1050
|
-
product: product,
|
|
1051
|
-
jwsRepresentation: jwsRepresentation
|
|
1052
|
-
)
|
|
1053
|
-
|
|
1054
|
-
// Finish transaction if requested
|
|
1055
|
-
if andDangerouslyFinishTransactionAutomatically {
|
|
1056
|
-
await transaction.finish()
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
return purchase
|
|
1060
|
-
|
|
1061
|
-
case .userCancelled:
|
|
1062
|
-
let errorJson = ErrorUtils.createErrorJson(
|
|
1063
|
-
code: IapErrorCode.userCancelled,
|
|
1064
|
-
message: "User cancelled the purchase",
|
|
1065
|
-
productId: sku
|
|
1066
|
-
)
|
|
1067
|
-
throw NSError(domain: "RnIap", code: 1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
1068
|
-
|
|
1069
|
-
case .pending:
|
|
1070
|
-
let errorJson = ErrorUtils.createErrorJson(
|
|
1071
|
-
code: IapErrorCode.deferredPayment,
|
|
1072
|
-
message: "The payment was deferred",
|
|
1073
|
-
productId: sku
|
|
1074
|
-
)
|
|
1075
|
-
throw NSError(domain: "RnIap", code: 2, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
1076
|
-
|
|
1077
|
-
@unknown default:
|
|
1078
|
-
let errorJson = ErrorUtils.createErrorJson(
|
|
1079
|
-
code: IapErrorCode.unknown,
|
|
1080
|
-
message: "Unknown purchase result",
|
|
1081
|
-
productId: sku
|
|
1082
|
-
)
|
|
1083
|
-
throw NSError(domain: "RnIap", code: 0, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
private func purchaseProductWithEvents(
|
|
1088
|
-
_ product: Product,
|
|
1089
|
-
sku: String,
|
|
1090
|
-
andDangerouslyFinishTransactionAutomatically: Bool,
|
|
1091
|
-
appAccountToken: String?,
|
|
1092
|
-
quantity: Double?,
|
|
1093
|
-
withOffer: [String: String]?
|
|
1094
|
-
) async throws {
|
|
1095
|
-
// Prepare purchase options
|
|
1096
|
-
var options: Set<Product.PurchaseOption> = []
|
|
1097
|
-
|
|
1098
|
-
// Add quantity if specified
|
|
1099
|
-
if let quantity = quantity, quantity > 0 {
|
|
1100
|
-
options.insert(.quantity(Int(quantity)))
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
// Add promotional offer if provided
|
|
1104
|
-
if let offerID = withOffer?["identifier"],
|
|
1105
|
-
let keyID = withOffer?["keyIdentifier"],
|
|
1106
|
-
let nonce = withOffer?["nonce"],
|
|
1107
|
-
let signature = withOffer?["signature"],
|
|
1108
|
-
let timestamp = withOffer?["timestamp"],
|
|
1109
|
-
let uuidNonce = UUID(uuidString: nonce),
|
|
1110
|
-
let signatureData = Data(base64Encoded: signature),
|
|
1111
|
-
let timestampInt = Int(timestamp) {
|
|
1112
|
-
options.insert(
|
|
1113
|
-
.promotionalOffer(
|
|
1114
|
-
offerID: offerID,
|
|
1115
|
-
keyID: keyID,
|
|
1116
|
-
nonce: uuidNonce,
|
|
1117
|
-
signature: signatureData,
|
|
1118
|
-
timestamp: timestampInt
|
|
1119
|
-
)
|
|
1120
|
-
)
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
// Add app account token if provided
|
|
1124
|
-
if let appAccountToken = appAccountToken,
|
|
1125
|
-
let appAccountUUID = UUID(uuidString: appAccountToken) {
|
|
1126
|
-
options.insert(.appAccountToken(appAccountUUID))
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
// Get window scene for iOS 17+ purchase confirmation
|
|
1130
|
-
let windowScene = await currentWindowScene()
|
|
1131
|
-
|
|
1132
|
-
// Perform the purchase
|
|
1133
|
-
let result: Product.PurchaseResult
|
|
1134
|
-
#if swift(>=5.9)
|
|
1135
|
-
if #available(iOS 17.0, tvOS 17.0, *) {
|
|
1136
|
-
if let windowScene = windowScene {
|
|
1137
|
-
result = try await product.purchase(confirmIn: windowScene, options: options)
|
|
1138
|
-
} else {
|
|
1139
|
-
result = try await product.purchase(options: options)
|
|
1140
|
-
}
|
|
1141
|
-
} else {
|
|
1142
|
-
#if !os(visionOS)
|
|
1143
|
-
result = try await product.purchase(options: options)
|
|
1144
|
-
#endif
|
|
1145
|
-
}
|
|
1146
|
-
#elseif !os(visionOS)
|
|
1147
|
-
result = try await product.purchase(options: options)
|
|
1148
|
-
#endif
|
|
1149
|
-
|
|
1150
|
-
switch result {
|
|
1151
|
-
case .success(let verification):
|
|
1152
|
-
let transaction = try checkVerified(verification)
|
|
1153
|
-
|
|
1154
|
-
// Store transaction if not auto-finishing
|
|
1155
|
-
if !andDangerouslyFinishTransactionAutomatically {
|
|
1156
|
-
self.transactions[String(transaction.id)] = transaction
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
// Get JWS representation
|
|
1160
|
-
let jwsRepresentation = verification.jwsRepresentation
|
|
1161
|
-
|
|
1162
|
-
// Create purchase object
|
|
1163
|
-
let purchase = self.convertToNitroPurchase(
|
|
1164
|
-
transaction,
|
|
1165
|
-
product: product,
|
|
1166
|
-
jwsRepresentation: jwsRepresentation
|
|
1167
|
-
)
|
|
1168
|
-
|
|
1169
|
-
// Finish transaction if requested
|
|
1170
|
-
if andDangerouslyFinishTransactionAutomatically {
|
|
1171
|
-
await transaction.finish()
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
// Send purchase update event
|
|
1175
|
-
self.sendPurchaseUpdate(purchase)
|
|
1176
|
-
|
|
1177
|
-
case .userCancelled:
|
|
1178
|
-
let error = self.createPurchaseErrorResult(
|
|
1179
|
-
code: IapErrorCode.userCancelled,
|
|
1180
|
-
message: "User cancelled the purchase",
|
|
1181
|
-
productId: sku
|
|
1182
|
-
)
|
|
1183
|
-
self.sendPurchaseError(error)
|
|
1184
|
-
|
|
1185
|
-
case .pending:
|
|
1186
|
-
let error = self.createPurchaseErrorResult(
|
|
1187
|
-
code: IapErrorCode.deferredPayment,
|
|
1188
|
-
message: "The payment was deferred",
|
|
1189
|
-
productId: sku
|
|
1190
|
-
)
|
|
1191
|
-
self.sendPurchaseError(error)
|
|
1192
|
-
|
|
1193
|
-
@unknown default:
|
|
1194
|
-
let error = self.createPurchaseErrorResult(
|
|
1195
|
-
code: IapErrorCode.unknown,
|
|
1196
|
-
message: "Unknown purchase result",
|
|
1197
|
-
productId: sku
|
|
1198
|
-
)
|
|
1199
|
-
self.sendPurchaseError(error)
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
|
|
1204
|
-
switch result {
|
|
1205
|
-
case .verified(let safe):
|
|
1206
|
-
return safe
|
|
1207
|
-
case .unverified(_, let error):
|
|
1208
|
-
let errorJson = ErrorUtils.createErrorJson(
|
|
1209
|
-
code: IapErrorCode.transactionValidationFailed,
|
|
1210
|
-
message: "Transaction verification failed: \(error)",
|
|
1211
|
-
underlyingError: error
|
|
1212
|
-
)
|
|
1213
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
578
|
private func ensureConnection() throws {
|
|
1218
579
|
guard isInitialized else {
|
|
1219
|
-
|
|
1220
|
-
code: IapErrorCode.notPrepared,
|
|
1221
|
-
message: "Connection not initialized. Call initConnection() first."
|
|
1222
|
-
)
|
|
1223
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
guard SKPaymentQueue.canMakePayments() else {
|
|
1227
|
-
let errorJson = ErrorUtils.createErrorJson(
|
|
1228
|
-
code: IapErrorCode.iapNotAvailable,
|
|
1229
|
-
message: "In-app purchases are not available on this device"
|
|
1230
|
-
)
|
|
1231
|
-
throw NSError(domain: "RnIap", code: -1, userInfo: [NSLocalizedDescriptionKey: errorJson])
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
@MainActor
|
|
1236
|
-
private func currentWindowScene() async -> UIWindowScene? {
|
|
1237
|
-
#if os(iOS) || os(tvOS)
|
|
1238
|
-
// Find the active window scene
|
|
1239
|
-
for scene in UIApplication.shared.connectedScenes {
|
|
1240
|
-
if let windowScene = scene as? UIWindowScene,
|
|
1241
|
-
windowScene.activationState == .foregroundActive {
|
|
1242
|
-
return windowScene
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
// Fallback to first window scene if no active one found
|
|
1247
|
-
for scene in UIApplication.shared.connectedScenes {
|
|
1248
|
-
if let windowScene = scene as? UIWindowScene {
|
|
1249
|
-
return windowScene
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
#endif
|
|
1253
|
-
return nil
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
private func convertToNitroProduct(_ storeProduct: StoreKit.Product, type: String) -> NitroProduct {
|
|
1257
|
-
var product = NitroProduct()
|
|
1258
|
-
|
|
1259
|
-
// Basic fields
|
|
1260
|
-
product.id = storeProduct.id
|
|
1261
|
-
product.title = storeProduct.displayName
|
|
1262
|
-
product.description = storeProduct.description
|
|
1263
|
-
// Map StoreKit type to cross-platform compatible string: "inapp" | "subs"
|
|
1264
|
-
// and set detailed iOS product type
|
|
1265
|
-
#if swift(>=5.7)
|
|
1266
|
-
switch storeProduct.type {
|
|
1267
|
-
case .consumable:
|
|
1268
|
-
product.type = "inapp"
|
|
1269
|
-
product.typeIOS = "consumable"
|
|
1270
|
-
case .nonConsumable:
|
|
1271
|
-
product.type = "inapp"
|
|
1272
|
-
product.typeIOS = "nonConsumable"
|
|
1273
|
-
case .autoRenewable:
|
|
1274
|
-
product.type = "subs"
|
|
1275
|
-
product.typeIOS = "autoRenewableSubscription"
|
|
1276
|
-
case .nonRenewable:
|
|
1277
|
-
product.type = "subs"
|
|
1278
|
-
product.typeIOS = "nonRenewingSubscription"
|
|
1279
|
-
default:
|
|
1280
|
-
product.type = type // fallback to requested filter
|
|
1281
|
-
product.typeIOS = nil
|
|
1282
|
-
}
|
|
1283
|
-
#else
|
|
1284
|
-
product.type = type
|
|
1285
|
-
#endif
|
|
1286
|
-
product.displayName = storeProduct.displayName
|
|
1287
|
-
product.displayPrice = storeProduct.displayPrice
|
|
1288
|
-
product.platform = "ios"
|
|
1289
|
-
|
|
1290
|
-
// Price and currency - priceFormatStyle.currencyCode is not optional
|
|
1291
|
-
product.currency = storeProduct.priceFormatStyle.currencyCode
|
|
1292
|
-
|
|
1293
|
-
// Convert Decimal price to Double - price is not optional
|
|
1294
|
-
product.price = NSDecimalNumber(decimal: storeProduct.price).doubleValue
|
|
1295
|
-
|
|
1296
|
-
// iOS specific fields
|
|
1297
|
-
product.isFamilyShareableIOS = storeProduct.isFamilyShareable
|
|
1298
|
-
|
|
1299
|
-
// Set JSON representation
|
|
1300
|
-
product.jsonRepresentationIOS = String(data: storeProduct.jsonRepresentation, encoding: .utf8)
|
|
1301
|
-
?? storeProduct.jsonRepresentation.base64EncodedString()
|
|
1302
|
-
|
|
1303
|
-
// Subscription information
|
|
1304
|
-
if let subscription = storeProduct.subscription {
|
|
1305
|
-
// Subscription period - value is Int, need to convert to Double
|
|
1306
|
-
product.subscriptionPeriodUnitIOS = getPeriodString(subscription.subscriptionPeriod.unit)
|
|
1307
|
-
product.subscriptionPeriodNumberIOS = Double(subscription.subscriptionPeriod.value)
|
|
1308
|
-
|
|
1309
|
-
// Introductory offer
|
|
1310
|
-
if let introOffer = subscription.introductoryOffer {
|
|
1311
|
-
product.introductoryPriceIOS = introOffer.displayPrice
|
|
1312
|
-
product.introductoryPriceAsAmountIOS = NSDecimalNumber(decimal: introOffer.price).doubleValue
|
|
1313
|
-
product.introductoryPricePaymentModeIOS = String(introOffer.paymentMode.rawValue)
|
|
1314
|
-
product.introductoryPriceNumberOfPeriodsIOS = Double(introOffer.periodCount)
|
|
1315
|
-
product.introductoryPriceSubscriptionPeriodIOS = getPeriodString(introOffer.period.unit)
|
|
1316
|
-
}
|
|
580
|
+
throw OpenIapFailure.restoreFailed(reason: "Connection not initialized. Call initConnection() first.")
|
|
1317
581
|
}
|
|
1318
|
-
|
|
1319
|
-
return product
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
private func convertProductToNitroProduct(_ product: Product, type: String) -> NitroProduct {
|
|
1323
|
-
return convertToNitroProduct(product, type: type)
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
private func getPeriodString(_ unit: StoreKit.Product.SubscriptionPeriod.Unit) -> String {
|
|
1327
|
-
switch unit {
|
|
1328
|
-
case .day: return "DAY"
|
|
1329
|
-
case .week: return "WEEK"
|
|
1330
|
-
case .month: return "MONTH"
|
|
1331
|
-
case .year: return "YEAR"
|
|
1332
|
-
@unknown default: return ""
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
private func getPurchaseState(_ transaction: Transaction) -> String {
|
|
1337
|
-
// Map StoreKit 2 transaction states to our unified PurchaseState enum
|
|
1338
|
-
if transaction.revocationDate != nil {
|
|
1339
|
-
return "failed"
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
// Check if transaction needs finishing (pending)
|
|
1343
|
-
// In StoreKit 2, transactions are automatically finished unless we handle them manually
|
|
1344
|
-
// We consider a transaction as "purchased" once it's verified
|
|
1345
|
-
|
|
1346
|
-
// Note: StoreKit 2 doesn't have direct equivalents for all states
|
|
1347
|
-
// - "restored" is handled separately through restore purchases flow
|
|
1348
|
-
// - "deferred" happens with parental controls but isn't exposed in Transaction
|
|
1349
|
-
// - "pending" isn't directly available in StoreKit 2
|
|
1350
|
-
|
|
1351
|
-
return "purchased"
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
private func convertToNitroPurchase(_ transaction: Transaction, product: StoreKit.Product, jwsRepresentation: String? = nil, renewalInfo: Product.SubscriptionInfo.RenewalInfo? = nil) -> NitroPurchase {
|
|
1355
|
-
var purchase = NitroPurchase()
|
|
1356
|
-
|
|
1357
|
-
// Basic fields
|
|
1358
|
-
purchase.id = String(transaction.id)
|
|
1359
|
-
purchase.productId = transaction.productID
|
|
1360
|
-
purchase.transactionDate = transaction.purchaseDate.timeIntervalSince1970 * 1000 // Convert to milliseconds
|
|
1361
|
-
purchase.platform = "ios"
|
|
1362
|
-
|
|
1363
|
-
// Common fields
|
|
1364
|
-
purchase.quantity = Double(transaction.purchasedQuantity)
|
|
1365
|
-
purchase.purchaseState = getPurchaseState(transaction)
|
|
1366
|
-
// For iOS, check renewal info first if available, otherwise fall back to expiration date check
|
|
1367
|
-
if let renewalInfo = renewalInfo {
|
|
1368
|
-
purchase.isAutoRenewing = renewalInfo.willAutoRenew
|
|
1369
|
-
} else {
|
|
1370
|
-
purchase.isAutoRenewing = (product.type == .autoRenewable && transaction.expirationDate != nil && transaction.expirationDate! > Date())
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
// iOS specific fields
|
|
1374
|
-
purchase.quantityIOS = Double(transaction.purchasedQuantity)
|
|
1375
|
-
|
|
1376
|
-
// originalID is not optional in StoreKit 2
|
|
1377
|
-
purchase.originalTransactionIdentifierIOS = String(transaction.originalID)
|
|
1378
|
-
|
|
1379
|
-
// originalPurchaseDate is not optional
|
|
1380
|
-
purchase.originalTransactionDateIOS = transaction.originalPurchaseDate.timeIntervalSince1970 * 1000
|
|
1381
|
-
|
|
1382
|
-
if let appAccountToken = transaction.appAccountToken {
|
|
1383
|
-
purchase.appAccountToken = appAccountToken.uuidString
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
// Store the JWS representation as purchaseToken for verification
|
|
1387
|
-
// JWS is passed from VerificationResult
|
|
1388
|
-
if let jws = jwsRepresentation {
|
|
1389
|
-
purchase.purchaseToken = jws
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
return purchase
|
|
1393
582
|
}
|
|
1394
583
|
|
|
1395
584
|
private func sendPurchaseUpdate(_ purchase: NitroPurchase) {
|
|
@@ -1418,65 +607,64 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
1418
607
|
updateListenerTask?.cancel()
|
|
1419
608
|
updateListenerTask = nil
|
|
1420
609
|
isInitialized = false
|
|
1421
|
-
productStore = nil
|
|
1422
|
-
transactions.removeAll()
|
|
1423
610
|
|
|
1424
|
-
// Remove payment observer if exists
|
|
1425
|
-
if let observer = paymentObserver {
|
|
1426
|
-
SKPaymentQueue.default().remove(observer)
|
|
1427
|
-
paymentObserver = nil
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
// Clear promoted products
|
|
1431
|
-
promotedProduct = nil
|
|
1432
|
-
promotedPayment = nil
|
|
1433
|
-
promotedSKProduct = nil
|
|
1434
611
|
|
|
612
|
+
// Remove OpenIAP listeners & end connection
|
|
613
|
+
if let sub = purchaseUpdatedSub { OpenIapModule.shared.removeListener(sub) }
|
|
614
|
+
if let sub = purchaseErrorSub { OpenIapModule.shared.removeListener(sub) }
|
|
615
|
+
if let sub = promotedProductSub { OpenIapModule.shared.removeListener(sub) }
|
|
616
|
+
purchaseUpdatedSub = nil
|
|
617
|
+
purchaseErrorSub = nil
|
|
618
|
+
promotedProductSub = nil
|
|
619
|
+
Task { _ = try? await OpenIapModule.shared.endConnection() }
|
|
620
|
+
|
|
1435
621
|
// Clear event listeners
|
|
1436
622
|
purchaseUpdatedListeners.removeAll()
|
|
1437
623
|
purchaseErrorListeners.removeAll()
|
|
1438
624
|
promotedProductListeners.removeAll()
|
|
1439
625
|
}
|
|
1440
|
-
|
|
1441
|
-
// Method called by PaymentObserver when a promoted product is received
|
|
1442
|
-
func handlePromotedProduct(payment: SKPayment, product: SKProduct) {
|
|
1443
|
-
self.promotedPayment = payment
|
|
1444
|
-
self.promotedSKProduct = product
|
|
1445
|
-
|
|
1446
|
-
// Convert SKProduct to StoreKit 2 Product and then to NitroProduct
|
|
1447
|
-
Task {
|
|
1448
|
-
if let products = try? await StoreKit.Product.products(for: [product.productIdentifier]),
|
|
1449
|
-
let storeKit2Product = products.first {
|
|
1450
|
-
self.promotedProduct = storeKit2Product
|
|
1451
|
-
|
|
1452
|
-
// Notify all promoted product listeners
|
|
1453
|
-
let nitroProduct = self.convertProductToNitroProduct(storeKit2Product, type: "inapp")
|
|
1454
|
-
for listener in self.promotedProductListeners {
|
|
1455
|
-
listener(nitroProduct)
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
626
|
|
|
1462
|
-
//
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
627
|
+
// MARK: - OpenIAP -> Nitro mappers
|
|
628
|
+
private func convertOpenIapProductToNitroProduct(_ p: OpenIapProduct) -> NitroProduct {
|
|
629
|
+
var n = NitroProduct()
|
|
630
|
+
n.id = p.id
|
|
631
|
+
n.title = p.title
|
|
632
|
+
n.description = p.description
|
|
633
|
+
n.type = p.type
|
|
634
|
+
n.displayName = p.displayName ?? p.displayNameIOS
|
|
635
|
+
n.displayPrice = p.displayPrice
|
|
636
|
+
n.currency = p.currency
|
|
637
|
+
n.price = p.price
|
|
638
|
+
n.platform = p.platform
|
|
639
|
+
// iOS specifics
|
|
640
|
+
n.typeIOS = p.typeIOS.rawValue
|
|
641
|
+
n.isFamilyShareableIOS = p.isFamilyShareableIOS
|
|
642
|
+
n.jsonRepresentationIOS = p.jsonRepresentationIOS
|
|
643
|
+
n.subscriptionPeriodUnitIOS = p.subscriptionPeriodUnitIOS
|
|
644
|
+
if let num = p.subscriptionPeriodNumberIOS, let d = Double(num) { n.subscriptionPeriodNumberIOS = d }
|
|
645
|
+
n.introductoryPriceIOS = p.introductoryPriceIOS
|
|
646
|
+
if let amt = p.introductoryPriceAsAmountIOS, let d = Double(amt) { n.introductoryPriceAsAmountIOS = d }
|
|
647
|
+
n.introductoryPricePaymentModeIOS = p.introductoryPricePaymentModeIOS
|
|
648
|
+
if let cnt = p.introductoryPriceNumberOfPeriodsIOS, let d = Double(cnt) { n.introductoryPriceNumberOfPeriodsIOS = d }
|
|
649
|
+
n.introductoryPriceSubscriptionPeriodIOS = p.introductoryPriceSubscriptionPeriodIOS
|
|
650
|
+
return n
|
|
1469
651
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
652
|
+
|
|
653
|
+
private func convertOpenIapPurchaseToNitroPurchase(_ p: OpenIapPurchase) -> NitroPurchase {
|
|
654
|
+
var n = NitroPurchase()
|
|
655
|
+
n.id = p.id
|
|
656
|
+
n.productId = p.productId
|
|
657
|
+
n.transactionDate = p.transactionDate
|
|
658
|
+
n.purchaseToken = p.purchaseToken
|
|
659
|
+
n.platform = p.platform
|
|
660
|
+
n.quantity = Double(p.quantity)
|
|
661
|
+
n.purchaseState = p.purchaseState.rawValue
|
|
662
|
+
n.isAutoRenewing = p.isAutoRenewing
|
|
663
|
+
// iOS specifics
|
|
664
|
+
if let q = p.quantityIOS { n.quantityIOS = Double(q) }
|
|
665
|
+
n.originalTransactionDateIOS = p.originalTransactionDateIOS
|
|
666
|
+
n.originalTransactionIdentifierIOS = p.originalTransactionIdentifierIOS
|
|
667
|
+
n.appAccountToken = p.appAccountToken
|
|
668
|
+
return n
|
|
1481
669
|
}
|
|
1482
670
|
}
|