react-native-iap 14.3.9 → 14.4.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 +11 -1
- package/README.md +2 -3
- package/android/build.gradle +24 -1
- package/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +369 -124
- package/android/src/main/java/com/margelo/nitro/iap/RnIapLog.kt +64 -0
- package/ios/HybridRnIap.swift +525 -362
- package/ios/RnIapHelper.swift +224 -0
- package/ios/RnIapLog.swift +127 -0
- package/lib/module/hooks/useIAP.js +2 -34
- package/lib/module/hooks/useIAP.js.map +1 -1
- package/lib/module/index.js +52 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/plugin/src/withIAP.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useIAP.d.ts +0 -12
- package/lib/typescript/src/hooks/useIAP.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +3 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/RnIap.nitro.d.ts +24 -0
- package/lib/typescript/src/specs/RnIap.nitro.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +8 -6
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JHybridRnIapSpec.cpp +64 -0
- package/nitrogen/generated/android/c++/JHybridRnIapSpec.hpp +4 -0
- package/nitrogen/generated/android/c++/JIapPlatform.hpp +3 -3
- package/nitrogen/generated/android/c++/JPurchaseAndroid.hpp +6 -2
- package/nitrogen/generated/android/c++/JPurchaseIOS.hpp +4 -0
- package/nitrogen/generated/android/c++/JPurchaseState.hpp +6 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/HybridRnIapSpec.kt +16 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/IapPlatform.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/PurchaseAndroid.kt +4 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/PurchaseIOS.kt +3 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/PurchaseState.kt +5 -5
- package/nitrogen/generated/ios/c++/HybridRnIapSpecSwift.hpp +32 -0
- package/nitrogen/generated/ios/swift/HybridRnIapSpec.swift +4 -0
- package/nitrogen/generated/ios/swift/HybridRnIapSpec_cxx.swift +82 -0
- package/nitrogen/generated/ios/swift/IapPlatform.swift +4 -4
- package/nitrogen/generated/ios/swift/PurchaseAndroid.swift +32 -2
- package/nitrogen/generated/ios/swift/PurchaseIOS.swift +13 -2
- package/nitrogen/generated/ios/swift/PurchaseState.swift +8 -8
- package/nitrogen/generated/shared/c++/HybridRnIapSpec.cpp +4 -0
- package/nitrogen/generated/shared/c++/HybridRnIapSpec.hpp +4 -0
- package/nitrogen/generated/shared/c++/IapPlatform.hpp +5 -5
- package/nitrogen/generated/shared/c++/PurchaseAndroid.hpp +6 -2
- package/nitrogen/generated/shared/c++/PurchaseIOS.hpp +5 -1
- package/nitrogen/generated/shared/c++/PurchaseState.hpp +11 -11
- package/openiap-versions.json +5 -0
- package/package.json +3 -2
- package/plugin/build/withIAP.js +35 -3
- package/plugin/src/withIAP.ts +44 -3
- package/src/hooks/useIAP.ts +3 -71
- package/src/index.ts +61 -2
- package/src/specs/RnIap.nitro.ts +28 -0
- package/src/types.ts +8 -6
package/ios/HybridRnIap.swift
CHANGED
|
@@ -5,26 +5,24 @@ import OpenIAP
|
|
|
5
5
|
@available(iOS 15.0, *)
|
|
6
6
|
class HybridRnIap: HybridRnIapSpec {
|
|
7
7
|
// MARK: - Properties
|
|
8
|
-
|
|
9
8
|
private var updateListenerTask: Task<Void, Never>?
|
|
10
9
|
private var isInitialized: Bool = false
|
|
11
10
|
private var isInitializing: Bool = false
|
|
12
|
-
|
|
11
|
+
private var productTypeBySku: [String: String] = [:]
|
|
13
12
|
// OpenIAP event subscriptions
|
|
14
13
|
private var purchaseUpdatedSub: Subscription?
|
|
15
14
|
private var purchaseErrorSub: Subscription?
|
|
16
15
|
private var promotedProductSub: Subscription?
|
|
17
|
-
|
|
18
|
-
// Promoted products are handled via OpenIAP listener
|
|
19
|
-
|
|
20
16
|
// Event listeners
|
|
21
17
|
private var purchaseUpdatedListeners: [(NitroPurchase) -> Void] = []
|
|
22
18
|
private var purchaseErrorListeners: [(NitroPurchaseResult) -> Void] = []
|
|
23
19
|
private var promotedProductListeners: [(NitroProduct) -> Void] = []
|
|
24
|
-
|
|
25
|
-
private var lastPurchaseErrorCode: String? = nil
|
|
26
|
-
private var lastPurchaseErrorProductId: String? = nil
|
|
20
|
+
private var lastPurchaseErrorKey: String? = nil
|
|
27
21
|
private var lastPurchaseErrorTimestamp: TimeInterval = 0
|
|
22
|
+
private var deliveredPurchaseEventKeys: Set<String> = []
|
|
23
|
+
private var deliveredPurchaseEventOrder: [String] = []
|
|
24
|
+
private let purchaseEventDedupLimit = 128
|
|
25
|
+
private var purchasePayloadById: [String: [String: Any]] = [:]
|
|
28
26
|
|
|
29
27
|
// MARK: - Initialization
|
|
30
28
|
|
|
@@ -42,131 +40,27 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
42
40
|
|
|
43
41
|
func initConnection() throws -> Promise<Bool> {
|
|
44
42
|
return Promise.async {
|
|
45
|
-
|
|
43
|
+
RnIapLog.payload("initConnection", nil)
|
|
44
|
+
self.attachListenersIfNeeded()
|
|
45
|
+
|
|
46
46
|
if self.isInitialized || self.isInitializing {
|
|
47
|
-
|
|
48
|
-
self.purchaseUpdatedSub = OpenIapModule.shared.purchaseUpdatedListener { [weak self] openIapPurchase in
|
|
49
|
-
guard let self else { return }
|
|
50
|
-
Task { @MainActor in
|
|
51
|
-
let nitro = self.convertOpenIapPurchaseToNitroPurchase(openIapPurchase)
|
|
52
|
-
self.sendPurchaseUpdate(nitro)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
if self.purchaseErrorSub == nil {
|
|
57
|
-
self.purchaseErrorSub = OpenIapModule.shared.purchaseErrorListener { [weak self] error in
|
|
58
|
-
guard let self else { return }
|
|
59
|
-
Task { @MainActor in
|
|
60
|
-
#if DEBUG
|
|
61
|
-
print("[HybridRnIap] purchaseError event: code=\(error.code), productId=\(error.productId ?? "-")")
|
|
62
|
-
#endif
|
|
63
|
-
let nitroError = self.createPurchaseErrorResult(
|
|
64
|
-
code: error.code,
|
|
65
|
-
message: error.message,
|
|
66
|
-
productId: error.productId
|
|
67
|
-
)
|
|
68
|
-
self.sendPurchaseError(nitroError, productId: error.productId)
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
if self.promotedProductSub == nil {
|
|
73
|
-
self.promotedProductSub = OpenIapModule.shared.promotedProductListenerIOS { [weak self] productId in
|
|
74
|
-
guard let self else { return }
|
|
75
|
-
Task {
|
|
76
|
-
do {
|
|
77
|
-
let req = OpenIapProductRequest(skus: [productId], type: "all")
|
|
78
|
-
let products = try await OpenIapModule.shared.fetchProducts(req)
|
|
79
|
-
if let p = products.first {
|
|
80
|
-
let nitro = self.convertOpenIapProductToNitroProduct(p)
|
|
81
|
-
await MainActor.run {
|
|
82
|
-
for listener in self.promotedProductListeners { listener(nitro) }
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
} catch {
|
|
86
|
-
let id = productId
|
|
87
|
-
await MainActor.run {
|
|
88
|
-
var minimal = NitroProduct()
|
|
89
|
-
minimal.id = id
|
|
90
|
-
minimal.title = id
|
|
91
|
-
minimal.type = "inapp"
|
|
92
|
-
minimal.platform = .ios
|
|
93
|
-
for listener in self.promotedProductListeners { listener(minimal) }
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
47
|
+
RnIapLog.result("initConnection", true)
|
|
99
48
|
return true
|
|
100
49
|
}
|
|
101
50
|
|
|
102
|
-
// Begin non-blocking initialization
|
|
103
51
|
self.isInitializing = true
|
|
104
|
-
// Pre-attach listeners so events are not missed
|
|
105
|
-
if self.purchaseUpdatedSub == nil {
|
|
106
|
-
self.purchaseUpdatedSub = OpenIapModule.shared.purchaseUpdatedListener { [weak self] openIapPurchase in
|
|
107
|
-
guard let self else { return }
|
|
108
|
-
Task { @MainActor in
|
|
109
|
-
let nitro = self.convertOpenIapPurchaseToNitroPurchase(openIapPurchase)
|
|
110
|
-
self.sendPurchaseUpdate(nitro)
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
if self.purchaseErrorSub == nil {
|
|
115
|
-
self.purchaseErrorSub = OpenIapModule.shared.purchaseErrorListener { [weak self] error in
|
|
116
|
-
guard let self else { return }
|
|
117
|
-
Task { @MainActor in
|
|
118
|
-
#if DEBUG
|
|
119
|
-
print("[HybridRnIap] purchaseError event: code=\(error.code), productId=\(error.productId ?? "-")")
|
|
120
|
-
#endif
|
|
121
|
-
let nitroError = self.createPurchaseErrorResult(
|
|
122
|
-
code: error.code,
|
|
123
|
-
message: error.message,
|
|
124
|
-
productId: error.productId
|
|
125
|
-
)
|
|
126
|
-
self.sendPurchaseError(nitroError, productId: error.productId)
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
if self.promotedProductSub == nil {
|
|
131
|
-
self.promotedProductSub = OpenIapModule.shared.promotedProductListenerIOS { [weak self] productId in
|
|
132
|
-
guard let self else { return }
|
|
133
|
-
Task {
|
|
134
|
-
do {
|
|
135
|
-
let req = OpenIapProductRequest(skus: [productId], type: "all")
|
|
136
|
-
let products = try await OpenIapModule.shared.fetchProducts(req)
|
|
137
|
-
if let p = products.first {
|
|
138
|
-
let nitro = self.convertOpenIapProductToNitroProduct(p)
|
|
139
|
-
await MainActor.run {
|
|
140
|
-
for listener in self.promotedProductListeners { listener(nitro) }
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
} catch {
|
|
144
|
-
let id = productId
|
|
145
|
-
await MainActor.run {
|
|
146
|
-
var minimal = NitroProduct()
|
|
147
|
-
minimal.id = id
|
|
148
|
-
minimal.title = id
|
|
149
|
-
minimal.type = "inapp"
|
|
150
|
-
minimal.platform = .ios
|
|
151
|
-
for listener in self.promotedProductListeners { listener(minimal) }
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
52
|
|
|
158
|
-
// Perform initialization and only report success when native init succeeds
|
|
159
53
|
do {
|
|
160
54
|
let ok = try await OpenIapModule.shared.initConnection()
|
|
55
|
+
RnIapLog.result("initConnection", ok)
|
|
161
56
|
self.isInitialized = ok
|
|
162
57
|
self.isInitializing = false
|
|
163
58
|
return ok
|
|
164
59
|
} catch {
|
|
165
|
-
|
|
166
|
-
let err =
|
|
167
|
-
code:
|
|
168
|
-
message: error.localizedDescription
|
|
169
|
-
productId: nil
|
|
60
|
+
RnIapLog.failure("initConnection", error: error)
|
|
61
|
+
let err = RnIapHelper.makePurchaseErrorResult(
|
|
62
|
+
code: .initConnection,
|
|
63
|
+
message: error.localizedDescription
|
|
170
64
|
)
|
|
171
65
|
self.sendPurchaseError(err, productId: nil)
|
|
172
66
|
self.isInitialized = false
|
|
@@ -185,97 +79,144 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
185
79
|
|
|
186
80
|
func fetchProducts(skus: [String], type: String) throws -> Promise<[NitroProduct]> {
|
|
187
81
|
return Promise.async {
|
|
188
|
-
|
|
189
|
-
|
|
82
|
+
try self.ensureConnection()
|
|
83
|
+
RnIapLog.payload("fetchProducts", [
|
|
84
|
+
"skus": skus,
|
|
85
|
+
"type": type
|
|
86
|
+
])
|
|
190
87
|
|
|
191
|
-
|
|
192
|
-
|
|
88
|
+
if skus.isEmpty {
|
|
89
|
+
throw PurchaseError.make(code: .emptySkuList)
|
|
90
|
+
}
|
|
193
91
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
case "inapp":
|
|
206
|
-
#if DEBUG
|
|
207
|
-
print("[HybridRnIap] fetchProducts received legacy type 'inapp'; forwarding as 'in-app'")
|
|
208
|
-
#endif
|
|
209
|
-
fallthrough
|
|
210
|
-
case "in-app":
|
|
211
|
-
resolvedType = "in-app"
|
|
212
|
-
case "subs":
|
|
213
|
-
resolvedType = "subs"
|
|
214
|
-
default:
|
|
215
|
-
break
|
|
216
|
-
}
|
|
92
|
+
var productsById: [String: NitroProduct] = [:]
|
|
93
|
+
let normalizedType = type.lowercased()
|
|
94
|
+
let queryTypes: [ProductQueryType]
|
|
95
|
+
if normalizedType == "all" {
|
|
96
|
+
queryTypes = [.inApp, .subs]
|
|
97
|
+
} else {
|
|
98
|
+
if normalizedType == "inapp" {
|
|
99
|
+
RnIapLog.warn("fetchProducts received legacy type 'inapp'; forwarding as 'in-app'")
|
|
100
|
+
}
|
|
101
|
+
queryTypes = [RnIapHelper.parseProductQueryType(type)]
|
|
102
|
+
}
|
|
217
103
|
|
|
218
|
-
|
|
219
|
-
|
|
104
|
+
for queryType in queryTypes {
|
|
105
|
+
let request = try OpenIapSerialization.productRequest(skus: skus, type: queryType)
|
|
106
|
+
RnIapLog.payload(
|
|
107
|
+
"fetchProducts.native", [
|
|
108
|
+
"skus": skus,
|
|
109
|
+
"type": queryType.rawValue
|
|
110
|
+
]
|
|
111
|
+
)
|
|
112
|
+
let result = try await OpenIapModule.shared.fetchProducts(request)
|
|
113
|
+
let payloads = RnIapHelper.sanitizeArray(OpenIapSerialization.products(result))
|
|
114
|
+
RnIapLog.result("fetchProducts.native", payloads)
|
|
115
|
+
for payload in payloads {
|
|
116
|
+
let nitroProduct = RnIapHelper.convertProductDictionary(payload)
|
|
117
|
+
productsById[nitroProduct.id] = nitroProduct
|
|
220
118
|
}
|
|
119
|
+
}
|
|
221
120
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
121
|
+
var products: [NitroProduct] = []
|
|
122
|
+
var seenIds = Set<String>()
|
|
123
|
+
for sku in skus {
|
|
124
|
+
if let product = productsById[sku], !seenIds.contains(product.id) {
|
|
125
|
+
products.append(product)
|
|
126
|
+
seenIds.insert(product.id)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
for product in productsById.values where !seenIds.contains(product.id) {
|
|
130
|
+
products.append(product)
|
|
131
|
+
seenIds.insert(product.id)
|
|
226
132
|
}
|
|
133
|
+
await MainActor.run {
|
|
134
|
+
products.forEach { self.productTypeBySku[$0.id] = $0.type.lowercased() }
|
|
135
|
+
}
|
|
136
|
+
RnIapLog.result(
|
|
137
|
+
"fetchProducts", products.map { ["id": $0.id, "type": $0.type] }
|
|
138
|
+
)
|
|
139
|
+
return products
|
|
227
140
|
}
|
|
228
141
|
}
|
|
229
142
|
|
|
230
143
|
func requestPurchase(request: NitroPurchaseRequest) throws -> Promise<RequestPurchaseResult?> {
|
|
231
144
|
return Promise.async {
|
|
232
145
|
let defaultResult: RequestPurchaseResult? = .third([])
|
|
146
|
+
RnIapLog.payload(
|
|
147
|
+
"requestPurchase", [
|
|
148
|
+
"hasIOS": request.ios != nil,
|
|
149
|
+
"hasAndroid": request.android != nil
|
|
150
|
+
]
|
|
151
|
+
)
|
|
152
|
+
|
|
233
153
|
guard let iosRequest = request.ios else {
|
|
234
|
-
let error =
|
|
235
|
-
code:
|
|
154
|
+
let error = RnIapHelper.makePurchaseErrorResult(
|
|
155
|
+
code: .developerError,
|
|
236
156
|
message: "No iOS request provided"
|
|
237
157
|
)
|
|
238
158
|
self.sendPurchaseError(error, productId: nil)
|
|
239
159
|
return defaultResult
|
|
240
160
|
}
|
|
161
|
+
|
|
162
|
+
guard self.isInitialized else {
|
|
163
|
+
let err = RnIapHelper.makePurchaseErrorResult(
|
|
164
|
+
code: .initConnection,
|
|
165
|
+
message: "IAP store connection not initialized",
|
|
166
|
+
iosRequest.sku
|
|
167
|
+
)
|
|
168
|
+
self.sendPurchaseError(err, productId: iosRequest.sku)
|
|
169
|
+
return defaultResult
|
|
170
|
+
}
|
|
171
|
+
|
|
241
172
|
do {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
#endif
|
|
247
|
-
let err = self.createPurchaseErrorResult(
|
|
248
|
-
code: OpenIapError.InitConnection,
|
|
249
|
-
message: "IAP store connection not initialized",
|
|
250
|
-
productId: iosRequest.sku
|
|
251
|
-
)
|
|
252
|
-
self.sendPurchaseError(err, productId: iosRequest.sku)
|
|
253
|
-
return defaultResult
|
|
173
|
+
var iosPayload: [String: Any] = ["sku": iosRequest.sku]
|
|
174
|
+
if let quantity = iosRequest.quantity { iosPayload["quantity"] = Int(quantity) }
|
|
175
|
+
if let finishAutomatically = iosRequest.andDangerouslyFinishTransactionAutomatically {
|
|
176
|
+
iosPayload["andDangerouslyFinishTransactionAutomatically"] = finishAutomatically
|
|
254
177
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
178
|
+
if let appAccountToken = iosRequest.appAccountToken {
|
|
179
|
+
iosPayload["appAccountToken"] = appAccountToken
|
|
180
|
+
}
|
|
181
|
+
if let withOffer = iosRequest.withOffer {
|
|
182
|
+
iosPayload["withOffer"] = withOffer
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let cachedType = await MainActor.run { self.productTypeBySku[iosRequest.sku] }
|
|
186
|
+
let resolvedType = RnIapHelper.parseProductQueryType(cachedType)
|
|
187
|
+
let purchaseType: ProductQueryType = resolvedType == .all ? .inApp : resolvedType
|
|
188
|
+
await MainActor.run {
|
|
189
|
+
self.productTypeBySku[iosRequest.sku] = purchaseType.rawValue
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let props = try RnIapHelper.decodeRequestPurchaseProps(
|
|
193
|
+
iosPayload: iosPayload,
|
|
194
|
+
type: purchaseType
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
RnIapLog.payload(
|
|
198
|
+
"requestPurchase.native", iosPayload
|
|
269
199
|
)
|
|
270
|
-
|
|
200
|
+
|
|
201
|
+
let result = try await OpenIapModule.shared.requestPurchase(props)
|
|
202
|
+
if result != nil {
|
|
203
|
+
RnIapLog.result("requestPurchase", "delegated to OpenIAP")
|
|
204
|
+
} else {
|
|
205
|
+
RnIapLog.result("requestPurchase", nil)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return defaultResult
|
|
209
|
+
} catch let purchaseError as PurchaseError {
|
|
210
|
+
RnIapLog.failure("requestPurchase", error: purchaseError)
|
|
211
|
+
// OpenIAP already publishes purchaseError events for PurchaseError instances.
|
|
212
|
+
// Avoid emitting a duplicate event back to JS; simply return.
|
|
271
213
|
return defaultResult
|
|
272
214
|
} catch {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
code: OpenIapError.ServiceError,
|
|
215
|
+
RnIapLog.failure("requestPurchase", error: error)
|
|
216
|
+
let err = RnIapHelper.makePurchaseErrorResult(
|
|
217
|
+
code: .purchaseError,
|
|
277
218
|
message: error.localizedDescription,
|
|
278
|
-
|
|
219
|
+
iosRequest.sku
|
|
279
220
|
)
|
|
280
221
|
self.sendPurchaseErrorDedup(err, productId: iosRequest.sku)
|
|
281
222
|
return defaultResult
|
|
@@ -287,13 +228,21 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
287
228
|
return Promise.async {
|
|
288
229
|
try self.ensureConnection()
|
|
289
230
|
do {
|
|
231
|
+
let alsoPublish = options?.ios?.alsoPublishToEventListener ?? false
|
|
290
232
|
let onlyActive = options?.ios?.onlyIncludeActiveItemsIOS ?? options?.ios?.onlyIncludeActiveItems ?? false
|
|
291
|
-
let
|
|
292
|
-
|
|
293
|
-
|
|
233
|
+
let optionsDictionary: [String: Any] = [
|
|
234
|
+
"alsoPublishToEventListenerIOS": alsoPublish,
|
|
235
|
+
"onlyIncludeActiveItemsIOS": onlyActive
|
|
236
|
+
]
|
|
237
|
+
let purchaseOptions = try OpenIapSerialization.purchaseOptions(from: optionsDictionary)
|
|
238
|
+
RnIapLog.payload("getAvailablePurchases", optionsDictionary)
|
|
239
|
+
let purchases = try await OpenIapModule.shared.getAvailablePurchases(purchaseOptions)
|
|
240
|
+
let payloads = RnIapHelper.sanitizeArray(OpenIapSerialization.purchases(purchases))
|
|
241
|
+
RnIapLog.result("getAvailablePurchases", payloads)
|
|
242
|
+
return payloads.map { RnIapHelper.convertPurchaseDictionary($0) }
|
|
294
243
|
} catch {
|
|
295
|
-
|
|
296
|
-
throw
|
|
244
|
+
RnIapLog.failure("getAvailablePurchases", error: error)
|
|
245
|
+
throw error
|
|
297
246
|
}
|
|
298
247
|
}
|
|
299
248
|
}
|
|
@@ -303,11 +252,32 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
303
252
|
guard let iosParams = params.ios else { return .first(true) }
|
|
304
253
|
try self.ensureConnection()
|
|
305
254
|
do {
|
|
306
|
-
|
|
307
|
-
|
|
255
|
+
RnIapLog.payload(
|
|
256
|
+
"finishTransaction", ["transactionId": iosParams.transactionId]
|
|
257
|
+
)
|
|
258
|
+
var purchasePayload = await MainActor.run { () -> [String: Any]? in
|
|
259
|
+
self.purchasePayloadById[iosParams.transactionId]
|
|
260
|
+
}
|
|
261
|
+
if purchasePayload == nil {
|
|
262
|
+
RnIapLog.warn("Missing cached purchase payload for \(iosParams.transactionId); falling back to identifier-only finish")
|
|
263
|
+
purchasePayload = ["transactionIdentifier": iosParams.transactionId]
|
|
264
|
+
}
|
|
265
|
+
guard let purchasePayload else {
|
|
266
|
+
throw PurchaseError.make(code: .purchaseError, message: "Missing purchase context for \(iosParams.transactionId)")
|
|
267
|
+
}
|
|
268
|
+
let sanitizedPayload = RnIapHelper.sanitizeDictionary(purchasePayload)
|
|
269
|
+
RnIapLog.payload("finishTransaction.nativePayload", sanitizedPayload)
|
|
270
|
+
let purchaseInput = try OpenIapSerialization.purchaseInput(from: purchasePayload)
|
|
271
|
+
try await OpenIapModule.shared.finishTransaction(purchase: purchaseInput, isConsumable: nil)
|
|
272
|
+
RnIapLog.result("finishTransaction", true)
|
|
273
|
+
await MainActor.run {
|
|
274
|
+
self.purchasePayloadById.removeValue(forKey: iosParams.transactionId)
|
|
275
|
+
}
|
|
276
|
+
return .first(true)
|
|
308
277
|
} catch {
|
|
278
|
+
RnIapLog.failure("finishTransaction", error: error)
|
|
309
279
|
let tid = iosParams.transactionId
|
|
310
|
-
throw
|
|
280
|
+
throw PurchaseError.make(code: .purchaseError, message: "Transaction not found: \(tid)")
|
|
311
281
|
}
|
|
312
282
|
}
|
|
313
283
|
}
|
|
@@ -315,10 +285,21 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
315
285
|
func validateReceipt(params: NitroReceiptValidationParams) throws -> Promise<Variant_NitroReceiptValidationResultIOS_NitroReceiptValidationResultAndroid> {
|
|
316
286
|
return Promise.async {
|
|
317
287
|
do {
|
|
318
|
-
|
|
288
|
+
RnIapLog.payload("validateReceiptIOS", ["sku": params.sku])
|
|
289
|
+
let props = try OpenIapSerialization.receiptValidationProps(from: ["sku": params.sku])
|
|
290
|
+
let result = try await OpenIapModule.shared.validateReceiptIOS(props)
|
|
291
|
+
var encoded = RnIapHelper.sanitizeDictionary(OpenIapSerialization.encode(result))
|
|
292
|
+
if encoded["receiptData"] != nil {
|
|
293
|
+
encoded["receiptData"] = "<receipt>"
|
|
294
|
+
}
|
|
295
|
+
if encoded["jwsRepresentation"] != nil {
|
|
296
|
+
encoded["jwsRepresentation"] = "<jws>"
|
|
297
|
+
}
|
|
298
|
+
RnIapLog.result("validateReceiptIOS", encoded)
|
|
319
299
|
var latest: NitroPurchase? = nil
|
|
320
|
-
if let
|
|
321
|
-
|
|
300
|
+
if let transaction = result.latestTransaction {
|
|
301
|
+
let payload = RnIapHelper.sanitizeDictionary(OpenIapSerialization.purchase(transaction))
|
|
302
|
+
latest = RnIapHelper.convertPurchaseDictionary(payload)
|
|
322
303
|
}
|
|
323
304
|
let mapped = NitroReceiptValidationResultIOS(
|
|
324
305
|
isValid: result.isValid,
|
|
@@ -328,7 +309,8 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
328
309
|
)
|
|
329
310
|
return .first(mapped)
|
|
330
311
|
} catch {
|
|
331
|
-
|
|
312
|
+
RnIapLog.failure("validateReceiptIOS", error: error)
|
|
313
|
+
throw PurchaseError.make(code: .receiptFailed, message: error.localizedDescription)
|
|
332
314
|
}
|
|
333
315
|
}
|
|
334
316
|
}
|
|
@@ -338,9 +320,13 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
338
320
|
func getStorefrontIOS() throws -> Promise<String> {
|
|
339
321
|
return Promise.async {
|
|
340
322
|
do {
|
|
341
|
-
|
|
323
|
+
RnIapLog.payload("getStorefrontIOS", nil)
|
|
324
|
+
let storefront = try await OpenIapModule.shared.getStorefrontIOS()
|
|
325
|
+
RnIapLog.result("getStorefrontIOS", storefront)
|
|
326
|
+
return storefront
|
|
342
327
|
} catch {
|
|
343
|
-
|
|
328
|
+
RnIapLog.failure("getStorefrontIOS", error: error)
|
|
329
|
+
throw PurchaseError.make(code: .serviceError, message: error.localizedDescription)
|
|
344
330
|
}
|
|
345
331
|
}
|
|
346
332
|
}
|
|
@@ -348,63 +334,74 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
348
334
|
func getAppTransactionIOS() throws -> Promise<String?> {
|
|
349
335
|
return Promise.async {
|
|
350
336
|
do {
|
|
337
|
+
RnIapLog.payload("getAppTransactionIOS", nil)
|
|
351
338
|
if #available(iOS 16.0, *) {
|
|
352
339
|
if let appTx = try await OpenIapModule.shared.getAppTransactionIOS() {
|
|
353
340
|
var result: [String: Any?] = [
|
|
354
341
|
"bundleId": appTx.bundleId,
|
|
355
342
|
"appVersion": appTx.appVersion,
|
|
356
343
|
"originalAppVersion": appTx.originalAppVersion,
|
|
357
|
-
"originalPurchaseDate": appTx.originalPurchaseDate
|
|
344
|
+
"originalPurchaseDate": appTx.originalPurchaseDate,
|
|
358
345
|
"deviceVerification": appTx.deviceVerification,
|
|
359
346
|
"deviceVerificationNonce": appTx.deviceVerificationNonce,
|
|
360
347
|
"environment": appTx.environment,
|
|
361
|
-
"signedDate": appTx.signedDate
|
|
348
|
+
"signedDate": appTx.signedDate,
|
|
362
349
|
"appId": appTx.appId,
|
|
363
350
|
"appVersionId": appTx.appVersionId,
|
|
364
|
-
"preorderDate": appTx.preorderDate
|
|
351
|
+
"preorderDate": appTx.preorderDate
|
|
365
352
|
]
|
|
366
353
|
result["appTransactionId"] = appTx.appTransactionId
|
|
367
354
|
result["originalPlatform"] = appTx.originalPlatform
|
|
368
355
|
let jsonData = try JSONSerialization.data(withJSONObject: result, options: [])
|
|
369
|
-
|
|
356
|
+
let string = String(data: jsonData, encoding: .utf8)
|
|
357
|
+
RnIapLog.result("getAppTransactionIOS", "<appTransaction>")
|
|
358
|
+
return string
|
|
370
359
|
}
|
|
360
|
+
RnIapLog.result("getAppTransactionIOS", nil)
|
|
371
361
|
return nil
|
|
372
362
|
} else {
|
|
363
|
+
RnIapLog.result("getAppTransactionIOS", nil)
|
|
373
364
|
return nil
|
|
374
365
|
}
|
|
375
366
|
} catch {
|
|
367
|
+
RnIapLog.failure("getAppTransactionIOS", error: error)
|
|
376
368
|
return nil
|
|
377
369
|
}
|
|
378
370
|
}
|
|
379
371
|
}
|
|
380
372
|
|
|
381
|
-
func
|
|
373
|
+
func getPromotedProductIOS() throws -> Promise<NitroProduct?> {
|
|
382
374
|
return Promise.async {
|
|
375
|
+
try self.ensureConnection()
|
|
383
376
|
do {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
n.description = p.localizedDescription
|
|
389
|
-
n.type = "inapp"
|
|
390
|
-
n.platform = .ios
|
|
391
|
-
n.price = p.price
|
|
392
|
-
n.currency = p.priceLocale.currencyCode
|
|
393
|
-
return n
|
|
377
|
+
RnIapLog.payload("getPromotedProductIOS", nil)
|
|
378
|
+
guard let product = try await OpenIapModule.shared.getPromotedProductIOS() else {
|
|
379
|
+
RnIapLog.result("getPromotedProductIOS", nil)
|
|
380
|
+
return nil
|
|
394
381
|
}
|
|
395
|
-
|
|
382
|
+
let payload = RnIapHelper.sanitizeDictionary(OpenIapSerialization.encode(product))
|
|
383
|
+
RnIapLog.result("getPromotedProductIOS", payload)
|
|
384
|
+
return RnIapHelper.convertProductDictionary(payload)
|
|
396
385
|
} catch {
|
|
397
|
-
|
|
386
|
+
RnIapLog.failure("getPromotedProductIOS", error: error)
|
|
387
|
+
throw PurchaseError.make(code: .serviceError, message: error.localizedDescription)
|
|
398
388
|
}
|
|
399
389
|
}
|
|
400
390
|
}
|
|
391
|
+
|
|
392
|
+
func requestPromotedProductIOS() throws -> Promise<NitroProduct?> {
|
|
393
|
+
return try getPromotedProductIOS()
|
|
394
|
+
}
|
|
401
395
|
|
|
402
396
|
func buyPromotedProductIOS() throws -> Promise<Void> {
|
|
403
397
|
return Promise.async {
|
|
404
398
|
do {
|
|
405
|
-
|
|
399
|
+
RnIapLog.payload("buyPromotedProductIOS", nil)
|
|
400
|
+
let ok = try await OpenIapModule.shared.requestPurchaseOnPromotedProductIOS()
|
|
401
|
+
RnIapLog.result("buyPromotedProductIOS", ok)
|
|
406
402
|
} catch {
|
|
407
403
|
// Event-only: OpenIAP will emit purchaseError for this flow. Avoid Promise rejection.
|
|
404
|
+
RnIapLog.failure("buyPromotedProductIOS", error: error)
|
|
408
405
|
}
|
|
409
406
|
}
|
|
410
407
|
}
|
|
@@ -412,11 +409,14 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
412
409
|
func presentCodeRedemptionSheetIOS() throws -> Promise<Bool> {
|
|
413
410
|
return Promise.async {
|
|
414
411
|
do {
|
|
412
|
+
RnIapLog.payload("presentCodeRedemptionSheetIOS", nil)
|
|
415
413
|
let ok = try await OpenIapModule.shared.presentCodeRedemptionSheetIOS()
|
|
414
|
+
RnIapLog.result("presentCodeRedemptionSheetIOS", ok)
|
|
416
415
|
return ok
|
|
417
416
|
} catch {
|
|
418
417
|
// Fallback with explicit error for simulator or unsupported cases
|
|
419
|
-
|
|
418
|
+
RnIapLog.failure("presentCodeRedemptionSheetIOS", error: error)
|
|
419
|
+
throw PurchaseError.make(code: .featureNotSupported)
|
|
420
420
|
}
|
|
421
421
|
}
|
|
422
422
|
}
|
|
@@ -424,9 +424,12 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
424
424
|
func clearTransactionIOS() throws -> Promise<Void> {
|
|
425
425
|
return Promise.async {
|
|
426
426
|
do {
|
|
427
|
-
|
|
427
|
+
RnIapLog.payload("clearTransactionIOS", nil)
|
|
428
|
+
let ok = try await OpenIapModule.shared.clearTransactionIOS()
|
|
429
|
+
RnIapLog.result("clearTransactionIOS", ok)
|
|
428
430
|
} catch {
|
|
429
431
|
// ignore
|
|
432
|
+
RnIapLog.failure("clearTransactionIOS", error: error)
|
|
430
433
|
}
|
|
431
434
|
}
|
|
432
435
|
}
|
|
@@ -437,35 +440,28 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
437
440
|
return Promise.async {
|
|
438
441
|
try self.ensureConnection()
|
|
439
442
|
do {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
default:
|
|
458
|
-
isActive = false
|
|
459
|
-
}
|
|
460
|
-
return NitroSubscriptionStatus(
|
|
461
|
-
state: isActive ? 1 : 0,
|
|
462
|
-
platform: "ios",
|
|
463
|
-
renewalInfo: renewal
|
|
464
|
-
)
|
|
443
|
+
RnIapLog.payload("subscriptionStatusIOS", ["sku": sku])
|
|
444
|
+
let statuses = try await OpenIapModule.shared.subscriptionStatusIOS(sku: sku)
|
|
445
|
+
let payloads = statuses.map { RnIapHelper.sanitizeDictionary(OpenIapSerialization.encode($0)) }
|
|
446
|
+
RnIapLog.result("subscriptionStatusIOS", payloads)
|
|
447
|
+
return payloads.map { payload in
|
|
448
|
+
let stateValue: Double
|
|
449
|
+
if let numeric = RnIapHelper.doubleValue(payload["state"]) {
|
|
450
|
+
stateValue = numeric
|
|
451
|
+
} else if let stateString = payload["state"] as? String {
|
|
452
|
+
stateValue = stateString.lowercased() == "subscribed" ? 1 : 0
|
|
453
|
+
} else {
|
|
454
|
+
stateValue = 0
|
|
455
|
+
}
|
|
456
|
+
let platform = payload["platform"] as? String ?? "ios"
|
|
457
|
+
var renewalInfo: NitroSubscriptionRenewalInfo? = nil
|
|
458
|
+
if let renewalPayload = payload["renewalInfo"] as? [String: Any?] {
|
|
459
|
+
renewalInfo = RnIapHelper.convertRenewalInfo(RnIapHelper.sanitizeDictionary(renewalPayload))
|
|
465
460
|
}
|
|
461
|
+
return NitroSubscriptionStatus(state: stateValue, platform: platform, renewalInfo: renewalInfo)
|
|
466
462
|
}
|
|
467
|
-
return []
|
|
468
463
|
} catch {
|
|
464
|
+
RnIapLog.failure("subscriptionStatusIOS", error: error)
|
|
469
465
|
return []
|
|
470
466
|
}
|
|
471
467
|
}
|
|
@@ -475,13 +471,24 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
475
471
|
return Promise.async {
|
|
476
472
|
try self.ensureConnection()
|
|
477
473
|
do {
|
|
474
|
+
RnIapLog.payload("currentEntitlementIOS", ["sku": sku])
|
|
478
475
|
let purchase = try await OpenIapModule.shared.currentEntitlementIOS(sku: sku)
|
|
479
476
|
if let purchase {
|
|
480
|
-
|
|
477
|
+
let raw = OpenIapSerialization.encode(purchase)
|
|
478
|
+
let payload = RnIapHelper.sanitizeDictionary(raw)
|
|
479
|
+
RnIapLog.result("currentEntitlementIOS", payload)
|
|
480
|
+
if let identifier = raw["id"] as? String {
|
|
481
|
+
await MainActor.run {
|
|
482
|
+
self.purchasePayloadById[identifier] = raw
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return RnIapHelper.convertPurchaseDictionary(payload)
|
|
481
486
|
}
|
|
487
|
+
RnIapLog.result("currentEntitlementIOS", nil)
|
|
482
488
|
return Optional<NitroPurchase>.none
|
|
483
489
|
} catch {
|
|
484
|
-
|
|
490
|
+
RnIapLog.failure("currentEntitlementIOS", error: error)
|
|
491
|
+
throw PurchaseError.make(code: .skuNotFound, productId: sku)
|
|
485
492
|
}
|
|
486
493
|
}
|
|
487
494
|
}
|
|
@@ -490,13 +497,24 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
490
497
|
return Promise.async {
|
|
491
498
|
try self.ensureConnection()
|
|
492
499
|
do {
|
|
500
|
+
RnIapLog.payload("latestTransactionIOS", ["sku": sku])
|
|
493
501
|
let purchase = try await OpenIapModule.shared.latestTransactionIOS(sku: sku)
|
|
494
502
|
if let purchase {
|
|
495
|
-
|
|
503
|
+
let raw = OpenIapSerialization.encode(purchase)
|
|
504
|
+
let payload = RnIapHelper.sanitizeDictionary(raw)
|
|
505
|
+
RnIapLog.result("latestTransactionIOS", payload)
|
|
506
|
+
if let identifier = raw["id"] as? String {
|
|
507
|
+
await MainActor.run {
|
|
508
|
+
self.purchasePayloadById[identifier] = raw
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return RnIapHelper.convertPurchaseDictionary(payload)
|
|
496
512
|
}
|
|
513
|
+
RnIapLog.result("latestTransactionIOS", nil)
|
|
497
514
|
return Optional<NitroPurchase>.none
|
|
498
515
|
} catch {
|
|
499
|
-
|
|
516
|
+
RnIapLog.failure("latestTransactionIOS", error: error)
|
|
517
|
+
throw PurchaseError.make(code: .skuNotFound, productId: sku)
|
|
500
518
|
}
|
|
501
519
|
}
|
|
502
520
|
}
|
|
@@ -504,9 +522,24 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
504
522
|
func getPendingTransactionsIOS() throws -> Promise<[NitroPurchase]> {
|
|
505
523
|
return Promise.async {
|
|
506
524
|
do {
|
|
525
|
+
RnIapLog.payload("getPendingTransactionsIOS", nil)
|
|
507
526
|
let pending = try await OpenIapModule.shared.getPendingTransactionsIOS()
|
|
508
|
-
|
|
527
|
+
var unionPurchases: [OpenIAP.Purchase] = []
|
|
528
|
+
for purchase in pending {
|
|
529
|
+
let union = OpenIAP.Purchase.purchaseIos(purchase)
|
|
530
|
+
unionPurchases.append(union)
|
|
531
|
+
let raw = OpenIapSerialization.purchase(union)
|
|
532
|
+
if let identifier = raw["id"] as? String {
|
|
533
|
+
await MainActor.run {
|
|
534
|
+
self.purchasePayloadById[identifier] = raw
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
let payloads = RnIapHelper.sanitizeArray(OpenIapSerialization.purchases(unionPurchases))
|
|
539
|
+
RnIapLog.result("getPendingTransactionsIOS", payloads)
|
|
540
|
+
return payloads.map { RnIapHelper.convertPurchaseDictionary($0) }
|
|
509
541
|
} catch {
|
|
542
|
+
RnIapLog.failure("getPendingTransactionsIOS", error: error)
|
|
510
543
|
return []
|
|
511
544
|
}
|
|
512
545
|
}
|
|
@@ -515,43 +548,115 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
515
548
|
func syncIOS() throws -> Promise<Bool> {
|
|
516
549
|
return Promise.async {
|
|
517
550
|
do {
|
|
551
|
+
RnIapLog.payload("syncIOS", nil)
|
|
518
552
|
let ok = try await OpenIapModule.shared.syncIOS()
|
|
553
|
+
RnIapLog.result("syncIOS", ok)
|
|
519
554
|
return ok
|
|
520
555
|
} catch {
|
|
521
|
-
|
|
556
|
+
RnIapLog.failure("syncIOS", error: error)
|
|
557
|
+
throw PurchaseError.make(code: .serviceError, message: error.localizedDescription)
|
|
522
558
|
}
|
|
523
559
|
}
|
|
524
560
|
}
|
|
525
561
|
|
|
526
562
|
func showManageSubscriptionsIOS() throws -> Promise<[NitroPurchase]> {
|
|
527
563
|
return Promise.async {
|
|
564
|
+
try self.ensureConnection()
|
|
528
565
|
do {
|
|
529
566
|
// Trigger system UI
|
|
567
|
+
RnIapLog.payload("showManageSubscriptionsIOS", nil)
|
|
530
568
|
_ = try await OpenIapModule.shared.showManageSubscriptionsIOS()
|
|
531
569
|
// Return current entitlements as approximation of updates
|
|
532
|
-
let
|
|
533
|
-
|
|
570
|
+
let optionsDictionary: [String: Any] = [
|
|
571
|
+
"alsoPublishToEventListenerIOS": false,
|
|
572
|
+
"onlyIncludeActiveItemsIOS": true
|
|
573
|
+
]
|
|
574
|
+
let iosOptions = try OpenIapSerialization.purchaseOptions(from: optionsDictionary)
|
|
575
|
+
let purchases = try await OpenIapModule.shared.getAvailablePurchases(iosOptions)
|
|
576
|
+
let payloads = RnIapHelper.sanitizeArray(OpenIapSerialization.purchases(purchases))
|
|
577
|
+
RnIapLog.result("showManageSubscriptionsIOS", payloads)
|
|
578
|
+
return payloads.map { RnIapHelper.convertPurchaseDictionary($0) }
|
|
579
|
+
} catch {
|
|
580
|
+
RnIapLog.failure("showManageSubscriptionsIOS", error: error)
|
|
581
|
+
throw PurchaseError.make(code: .serviceError, message: error.localizedDescription)
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
func deepLinkToSubscriptionsIOS() throws -> Promise<Bool> {
|
|
587
|
+
return Promise.async {
|
|
588
|
+
try self.ensureConnection()
|
|
589
|
+
do {
|
|
590
|
+
RnIapLog.payload("deepLinkToSubscriptionsIOS", nil)
|
|
591
|
+
try await OpenIapModule.shared.deepLinkToSubscriptions(nil)
|
|
592
|
+
RnIapLog.result("deepLinkToSubscriptionsIOS", true)
|
|
593
|
+
return true
|
|
534
594
|
} catch {
|
|
535
|
-
|
|
595
|
+
RnIapLog.failure("deepLinkToSubscriptionsIOS", error: error)
|
|
596
|
+
throw PurchaseError.make(code: .serviceError, message: error.localizedDescription)
|
|
536
597
|
}
|
|
537
598
|
}
|
|
538
599
|
}
|
|
539
600
|
|
|
540
601
|
func isEligibleForIntroOfferIOS(groupID: String) throws -> Promise<Bool> {
|
|
541
602
|
return Promise.async {
|
|
542
|
-
|
|
603
|
+
RnIapLog.payload("isEligibleForIntroOfferIOS", ["groupID": groupID])
|
|
604
|
+
let value = try await OpenIapModule.shared.isEligibleForIntroOfferIOS(groupID: groupID)
|
|
605
|
+
RnIapLog.result("isEligibleForIntroOfferIOS", value)
|
|
606
|
+
return value
|
|
543
607
|
}
|
|
544
608
|
}
|
|
545
609
|
|
|
546
610
|
func getReceiptDataIOS() throws -> Promise<String> {
|
|
547
611
|
return Promise.async {
|
|
612
|
+
try self.ensureConnection()
|
|
548
613
|
do {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
614
|
+
RnIapLog.payload("getReceiptDataIOS", nil)
|
|
615
|
+
let receipt = try await RnIapHelper.loadReceiptData(refresh: false)
|
|
616
|
+
RnIapLog.result("getReceiptDataIOS", "<receipt>")
|
|
617
|
+
return receipt
|
|
618
|
+
} catch let purchaseError as PurchaseError {
|
|
619
|
+
RnIapLog.failure("getReceiptDataIOS", error: purchaseError)
|
|
620
|
+
throw purchaseError
|
|
621
|
+
} catch {
|
|
622
|
+
RnIapLog.failure("getReceiptDataIOS", error: error)
|
|
623
|
+
throw PurchaseError.make(code: .receiptFailed, message: error.localizedDescription)
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
func getReceiptIOS() throws -> Promise<String> {
|
|
629
|
+
return Promise.async {
|
|
630
|
+
try self.ensureConnection()
|
|
631
|
+
do {
|
|
632
|
+
RnIapLog.payload("getReceiptIOS", nil)
|
|
633
|
+
let receipt = try await RnIapHelper.loadReceiptData(refresh: true)
|
|
634
|
+
RnIapLog.result("getReceiptIOS", "<receipt>")
|
|
635
|
+
return receipt
|
|
636
|
+
} catch let purchaseError as PurchaseError {
|
|
637
|
+
RnIapLog.failure("getReceiptIOS", error: purchaseError)
|
|
638
|
+
throw purchaseError
|
|
639
|
+
} catch {
|
|
640
|
+
RnIapLog.failure("getReceiptIOS", error: error)
|
|
641
|
+
throw PurchaseError.make(code: .receiptFailed, message: error.localizedDescription)
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
func requestReceiptRefreshIOS() throws -> Promise<String> {
|
|
647
|
+
return Promise.async {
|
|
648
|
+
try self.ensureConnection()
|
|
649
|
+
do {
|
|
650
|
+
RnIapLog.payload("requestReceiptRefreshIOS", nil)
|
|
651
|
+
let receipt = try await RnIapHelper.loadReceiptData(refresh: true)
|
|
652
|
+
RnIapLog.result("requestReceiptRefreshIOS", "<receipt>")
|
|
653
|
+
return receipt
|
|
654
|
+
} catch let purchaseError as PurchaseError {
|
|
655
|
+
RnIapLog.failure("requestReceiptRefreshIOS", error: purchaseError)
|
|
656
|
+
throw purchaseError
|
|
553
657
|
} catch {
|
|
554
|
-
|
|
658
|
+
RnIapLog.failure("requestReceiptRefreshIOS", error: error)
|
|
659
|
+
throw PurchaseError.make(code: .receiptFailed, message: error.localizedDescription)
|
|
555
660
|
}
|
|
556
661
|
}
|
|
557
662
|
}
|
|
@@ -559,7 +664,10 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
559
664
|
func isTransactionVerifiedIOS(sku: String) throws -> Promise<Bool> {
|
|
560
665
|
return Promise.async {
|
|
561
666
|
try self.ensureConnection()
|
|
562
|
-
|
|
667
|
+
RnIapLog.payload("isTransactionVerifiedIOS", ["sku": sku])
|
|
668
|
+
let value = try await OpenIapModule.shared.isTransactionVerifiedIOS(sku: sku)
|
|
669
|
+
RnIapLog.result("isTransactionVerifiedIOS", value)
|
|
670
|
+
return value
|
|
563
671
|
}
|
|
564
672
|
}
|
|
565
673
|
|
|
@@ -567,17 +675,29 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
567
675
|
return Promise.async {
|
|
568
676
|
try self.ensureConnection()
|
|
569
677
|
do {
|
|
678
|
+
RnIapLog.payload("getTransactionJwsIOS", ["sku": sku])
|
|
570
679
|
let jws = try await OpenIapModule.shared.getTransactionJwsIOS(sku: sku)
|
|
680
|
+
let maskedJws: Any? = (jws == nil) ? nil : "<jws>"
|
|
681
|
+
RnIapLog.result("getTransactionJwsIOS", maskedJws)
|
|
571
682
|
return jws
|
|
572
683
|
} catch {
|
|
573
|
-
|
|
684
|
+
RnIapLog.failure("getTransactionJwsIOS", error: error)
|
|
685
|
+
throw PurchaseError.make(code: .transactionValidationFailed, message: "Can't find transaction for sku \(sku)")
|
|
574
686
|
}
|
|
575
687
|
}
|
|
576
688
|
}
|
|
577
689
|
|
|
578
690
|
func beginRefundRequestIOS(sku: String) throws -> Promise<String?> {
|
|
579
691
|
return Promise.async {
|
|
580
|
-
do {
|
|
692
|
+
do {
|
|
693
|
+
RnIapLog.payload("beginRefundRequestIOS", ["sku": sku])
|
|
694
|
+
let result = try await OpenIapModule.shared.beginRefundRequestIOS(sku: sku)
|
|
695
|
+
RnIapLog.result("beginRefundRequestIOS", result)
|
|
696
|
+
return result
|
|
697
|
+
} catch {
|
|
698
|
+
RnIapLog.failure("beginRefundRequestIOS", error: error)
|
|
699
|
+
return nil
|
|
700
|
+
}
|
|
581
701
|
}
|
|
582
702
|
}
|
|
583
703
|
|
|
@@ -586,23 +706,12 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
586
706
|
|
|
587
707
|
// If a promoted product is already available from OpenIAP, notify immediately
|
|
588
708
|
Task {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
let
|
|
592
|
-
|
|
593
|
-
let
|
|
594
|
-
|
|
595
|
-
await MainActor.run {
|
|
596
|
-
var n = NitroProduct()
|
|
597
|
-
n.id = id
|
|
598
|
-
n.title = title
|
|
599
|
-
n.description = desc
|
|
600
|
-
n.type = "inapp"
|
|
601
|
-
n.platform = .ios
|
|
602
|
-
n.price = price
|
|
603
|
-
n.currency = currency
|
|
604
|
-
listener(n)
|
|
605
|
-
}
|
|
709
|
+
RnIapLog.payload("promotedProductListenerIOS.fetch", nil)
|
|
710
|
+
if let product = try? await OpenIapModule.shared.getPromotedProductIOS() {
|
|
711
|
+
let payload = RnIapHelper.sanitizeDictionary(OpenIapSerialization.encode(product))
|
|
712
|
+
RnIapLog.result("promotedProductListenerIOS.fetch", payload)
|
|
713
|
+
let nitro = RnIapHelper.convertProductDictionary(payload)
|
|
714
|
+
await MainActor.run { listener(nitro) }
|
|
606
715
|
}
|
|
607
716
|
}
|
|
608
717
|
}
|
|
@@ -636,24 +745,127 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
636
745
|
}
|
|
637
746
|
|
|
638
747
|
// MARK: - Private Helper Methods
|
|
639
|
-
|
|
748
|
+
|
|
749
|
+
private func attachListenersIfNeeded() {
|
|
750
|
+
if purchaseUpdatedSub == nil {
|
|
751
|
+
RnIapLog.payload("purchaseUpdatedListener.register", nil)
|
|
752
|
+
purchaseUpdatedSub = OpenIapModule.shared.purchaseUpdatedListener { [weak self] openIapPurchase in
|
|
753
|
+
guard let self else { return }
|
|
754
|
+
Task { @MainActor in
|
|
755
|
+
let rawPayload = OpenIapSerialization.purchase(openIapPurchase)
|
|
756
|
+
let payload = RnIapHelper.sanitizeDictionary(rawPayload)
|
|
757
|
+
RnIapLog.result("purchaseUpdatedListener", payload)
|
|
758
|
+
if let identifier = rawPayload["id"] as? String {
|
|
759
|
+
self.purchasePayloadById[identifier] = rawPayload
|
|
760
|
+
}
|
|
761
|
+
let nitro = RnIapHelper.convertPurchaseDictionary(payload)
|
|
762
|
+
self.sendPurchaseUpdate(nitro)
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
RnIapLog.result("purchaseUpdatedListener.register", "attached")
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if purchaseErrorSub == nil {
|
|
769
|
+
RnIapLog.payload("purchaseErrorListener.register", nil)
|
|
770
|
+
purchaseErrorSub = OpenIapModule.shared.purchaseErrorListener { [weak self] error in
|
|
771
|
+
guard let self else { return }
|
|
772
|
+
Task { @MainActor in
|
|
773
|
+
let payload = RnIapHelper.sanitizeDictionary(OpenIapSerialization.encode(error))
|
|
774
|
+
RnIapLog.result("purchaseErrorListener", payload)
|
|
775
|
+
let nitroError = RnIapHelper.makePurchaseErrorResult(
|
|
776
|
+
code: error.code,
|
|
777
|
+
message: error.message,
|
|
778
|
+
error.productId
|
|
779
|
+
)
|
|
780
|
+
self.sendPurchaseError(nitroError, productId: error.productId)
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
RnIapLog.result("purchaseErrorListener.register", "attached")
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if promotedProductSub == nil {
|
|
787
|
+
RnIapLog.payload("promotedProductListenerIOS.register", nil)
|
|
788
|
+
promotedProductSub = OpenIapModule.shared.promotedProductListenerIOS { [weak self] productId in
|
|
789
|
+
guard let self else { return }
|
|
790
|
+
Task {
|
|
791
|
+
RnIapLog.payload("promotedProductListenerIOS", ["productId": productId])
|
|
792
|
+
do {
|
|
793
|
+
let request = try OpenIapSerialization.productRequest(skus: [productId], type: .all)
|
|
794
|
+
let result = try await OpenIapModule.shared.fetchProducts(request)
|
|
795
|
+
let payloads = RnIapHelper.sanitizeArray(OpenIapSerialization.products(result))
|
|
796
|
+
RnIapLog.result("fetchProducts", payloads)
|
|
797
|
+
if let payload = payloads.first {
|
|
798
|
+
let nitro = RnIapHelper.convertProductDictionary(payload)
|
|
799
|
+
await MainActor.run {
|
|
800
|
+
for listener in self.promotedProductListeners { listener(nitro) }
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
} catch {
|
|
804
|
+
RnIapLog.failure("promotedProductListenerIOS", error: error)
|
|
805
|
+
let id = productId
|
|
806
|
+
await MainActor.run {
|
|
807
|
+
var minimal = NitroProduct()
|
|
808
|
+
minimal.id = id
|
|
809
|
+
minimal.title = id
|
|
810
|
+
minimal.type = "inapp"
|
|
811
|
+
minimal.platform = .ios
|
|
812
|
+
for listener in self.promotedProductListeners { listener(minimal) }
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
RnIapLog.result("promotedProductListenerIOS.register", "attached")
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
640
821
|
private func ensureConnection() throws {
|
|
641
822
|
guard isInitialized else {
|
|
642
|
-
throw
|
|
823
|
+
throw PurchaseError.make(code: .initConnection, message: "Connection not initialized. Call initConnection() first.")
|
|
643
824
|
}
|
|
644
825
|
}
|
|
645
826
|
|
|
646
827
|
private func sendPurchaseUpdate(_ purchase: NitroPurchase) {
|
|
828
|
+
let keyComponents = [
|
|
829
|
+
purchase.id,
|
|
830
|
+
purchase.productId,
|
|
831
|
+
String(purchase.transactionDate),
|
|
832
|
+
purchase.originalTransactionIdentifierIOS ?? "",
|
|
833
|
+
purchase.purchaseToken ?? ""
|
|
834
|
+
]
|
|
835
|
+
let eventKey = keyComponents.joined(separator: "#")
|
|
836
|
+
|
|
837
|
+
if deliveredPurchaseEventKeys.contains(eventKey) {
|
|
838
|
+
RnIapLog.warn("Duplicate purchase update skipped for \(purchase.productId)")
|
|
839
|
+
return
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
deliveredPurchaseEventKeys.insert(eventKey)
|
|
843
|
+
deliveredPurchaseEventOrder.append(eventKey)
|
|
844
|
+
if deliveredPurchaseEventOrder.count > purchaseEventDedupLimit, let removed = deliveredPurchaseEventOrder.first {
|
|
845
|
+
deliveredPurchaseEventOrder.removeFirst()
|
|
846
|
+
deliveredPurchaseEventKeys.remove(removed)
|
|
847
|
+
}
|
|
848
|
+
|
|
647
849
|
for listener in purchaseUpdatedListeners {
|
|
648
850
|
listener(purchase)
|
|
649
851
|
}
|
|
650
852
|
}
|
|
651
853
|
|
|
652
854
|
private func sendPurchaseError(_ error: NitroPurchaseResult, productId: String? = nil) {
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
855
|
+
let now = Date().timeIntervalSince1970
|
|
856
|
+
let dedupIdentifier = productId
|
|
857
|
+
?? (error.purchaseToken?.isEmpty == false ? error.purchaseToken : nil)
|
|
858
|
+
?? (error.message.isEmpty ? nil : error.message)
|
|
859
|
+
let currentKey = RnIapHelper.makeErrorDedupKey(code: error.code, productId: dedupIdentifier)
|
|
860
|
+
// Dedup only when the exact same error is emitted almost simultaneously.
|
|
861
|
+
let withinWindow = (now - lastPurchaseErrorTimestamp) < 0.15
|
|
862
|
+
if currentKey == lastPurchaseErrorKey && withinWindow {
|
|
863
|
+
return
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
lastPurchaseErrorKey = currentKey
|
|
867
|
+
lastPurchaseErrorTimestamp = now
|
|
868
|
+
|
|
657
869
|
// Ensure we never leak SKU via purchaseToken
|
|
658
870
|
var sanitized = error
|
|
659
871
|
if let pid = productId, sanitized.purchaseToken == pid {
|
|
@@ -665,26 +877,9 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
665
877
|
}
|
|
666
878
|
|
|
667
879
|
private func sendPurchaseErrorDedup(_ error: NitroPurchaseResult, productId: String? = nil) {
|
|
668
|
-
let now = Date().timeIntervalSince1970
|
|
669
|
-
let sameCode = (error.code == lastPurchaseErrorCode)
|
|
670
|
-
let sameProduct = (productId == lastPurchaseErrorProductId)
|
|
671
|
-
let withinWindow = (now - lastPurchaseErrorTimestamp) < 0.3
|
|
672
|
-
if sameCode && sameProduct && withinWindow {
|
|
673
|
-
return
|
|
674
|
-
}
|
|
675
880
|
sendPurchaseError(error, productId: productId)
|
|
676
881
|
}
|
|
677
882
|
|
|
678
|
-
private func createPurchaseErrorResult(code: String, message: String, productId: String? = nil) -> NitroPurchaseResult {
|
|
679
|
-
var result = NitroPurchaseResult()
|
|
680
|
-
result.responseCode = 0
|
|
681
|
-
result.code = code
|
|
682
|
-
result.message = message
|
|
683
|
-
// Do not overload the token field with productId
|
|
684
|
-
result.purchaseToken = nil
|
|
685
|
-
return result
|
|
686
|
-
}
|
|
687
|
-
|
|
688
883
|
private func cleanupExistingState() {
|
|
689
884
|
// Cancel transaction listener if any
|
|
690
885
|
updateListenerTask?.cancel()
|
|
@@ -693,68 +888,36 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
693
888
|
|
|
694
889
|
|
|
695
890
|
// Remove OpenIAP listeners & end connection
|
|
696
|
-
if let sub = purchaseUpdatedSub {
|
|
697
|
-
|
|
698
|
-
|
|
891
|
+
if let sub = purchaseUpdatedSub {
|
|
892
|
+
RnIapLog.payload("removeListener", "purchaseUpdated")
|
|
893
|
+
OpenIapModule.shared.removeListener(sub)
|
|
894
|
+
}
|
|
895
|
+
if let sub = purchaseErrorSub {
|
|
896
|
+
RnIapLog.payload("removeListener", "purchaseError")
|
|
897
|
+
OpenIapModule.shared.removeListener(sub)
|
|
898
|
+
}
|
|
899
|
+
if let sub = promotedProductSub {
|
|
900
|
+
RnIapLog.payload("removeListener", "promotedProduct")
|
|
901
|
+
OpenIapModule.shared.removeListener(sub)
|
|
902
|
+
}
|
|
699
903
|
purchaseUpdatedSub = nil
|
|
700
904
|
purchaseErrorSub = nil
|
|
701
905
|
promotedProductSub = nil
|
|
702
|
-
Task {
|
|
906
|
+
Task {
|
|
907
|
+
RnIapLog.payload("endConnection", nil)
|
|
908
|
+
let result = try? await OpenIapModule.shared.endConnection()
|
|
909
|
+
RnIapLog.result("endConnection", result as Any)
|
|
910
|
+
}
|
|
703
911
|
|
|
704
912
|
// Clear event listeners
|
|
705
913
|
purchaseUpdatedListeners.removeAll()
|
|
706
914
|
purchaseErrorListeners.removeAll()
|
|
707
915
|
promotedProductListeners.removeAll()
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
n.id = p.id
|
|
714
|
-
n.title = p.title
|
|
715
|
-
n.description = p.description
|
|
716
|
-
n.type = p.type
|
|
717
|
-
n.displayName = p.displayName ?? p.displayNameIOS
|
|
718
|
-
n.displayPrice = p.displayPrice
|
|
719
|
-
n.currency = p.currency
|
|
720
|
-
n.price = p.price
|
|
721
|
-
if let platform = IapPlatform(fromString: p.platform) {
|
|
722
|
-
n.platform = platform
|
|
723
|
-
}
|
|
724
|
-
// iOS specifics
|
|
725
|
-
n.typeIOS = p.typeIOS.rawValue
|
|
726
|
-
n.isFamilyShareableIOS = p.isFamilyShareableIOS
|
|
727
|
-
n.jsonRepresentationIOS = p.jsonRepresentationIOS
|
|
728
|
-
n.subscriptionPeriodUnitIOS = p.subscriptionPeriodUnitIOS
|
|
729
|
-
if let num = p.subscriptionPeriodNumberIOS, let d = Double(num) { n.subscriptionPeriodNumberIOS = d }
|
|
730
|
-
n.introductoryPriceIOS = p.introductoryPriceIOS
|
|
731
|
-
if let amt = p.introductoryPriceAsAmountIOS, let d = Double(amt) { n.introductoryPriceAsAmountIOS = d }
|
|
732
|
-
n.introductoryPricePaymentModeIOS = p.introductoryPricePaymentModeIOS
|
|
733
|
-
if let cnt = p.introductoryPriceNumberOfPeriodsIOS, let d = Double(cnt) { n.introductoryPriceNumberOfPeriodsIOS = d }
|
|
734
|
-
n.introductoryPriceSubscriptionPeriodIOS = p.introductoryPriceSubscriptionPeriodIOS
|
|
735
|
-
return n
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
private func convertOpenIapPurchaseToNitroPurchase(_ p: OpenIapPurchase) -> NitroPurchase {
|
|
739
|
-
var n = NitroPurchase()
|
|
740
|
-
n.id = p.id
|
|
741
|
-
n.productId = p.productId
|
|
742
|
-
n.transactionDate = p.transactionDate
|
|
743
|
-
n.purchaseToken = p.purchaseToken
|
|
744
|
-
if let platform = IapPlatform(fromString: p.platform) {
|
|
745
|
-
n.platform = platform
|
|
746
|
-
}
|
|
747
|
-
n.quantity = Double(p.quantity)
|
|
748
|
-
if let state = PurchaseState(fromString: p.purchaseState.rawValue) {
|
|
749
|
-
n.purchaseState = state
|
|
750
|
-
}
|
|
751
|
-
n.isAutoRenewing = p.isAutoRenewing
|
|
752
|
-
// iOS specifics
|
|
753
|
-
if let q = p.quantityIOS { n.quantityIOS = Double(q) }
|
|
754
|
-
n.originalTransactionDateIOS = p.originalTransactionDateIOS
|
|
755
|
-
n.originalTransactionIdentifierIOS = p.originalTransactionIdentifierIOS
|
|
756
|
-
n.appAccountToken = p.appAccountToken
|
|
757
|
-
return n
|
|
916
|
+
deliveredPurchaseEventKeys.removeAll()
|
|
917
|
+
deliveredPurchaseEventOrder.removeAll()
|
|
918
|
+
purchasePayloadById.removeAll()
|
|
919
|
+
lastPurchaseErrorKey = nil
|
|
920
|
+
lastPurchaseErrorTimestamp = 0
|
|
758
921
|
}
|
|
759
922
|
|
|
760
923
|
// MARK: - Android-only stubs (required for protocol conformance)
|
|
@@ -763,13 +926,13 @@ class HybridRnIap: HybridRnIapSpec {
|
|
|
763
926
|
// because the TS spec marks them as Android-only.
|
|
764
927
|
func getStorefrontAndroid() throws -> Promise<String> {
|
|
765
928
|
return Promise.async {
|
|
766
|
-
throw
|
|
929
|
+
throw PurchaseError.make(code: .featureNotSupported)
|
|
767
930
|
}
|
|
768
931
|
}
|
|
769
932
|
|
|
770
933
|
func deepLinkToSubscriptionsAndroid(options: NitroDeepLinkOptionsAndroid) throws -> Promise<Void> {
|
|
771
934
|
return Promise.async {
|
|
772
|
-
throw
|
|
935
|
+
throw PurchaseError.make(code: .featureNotSupported)
|
|
773
936
|
}
|
|
774
937
|
}
|
|
775
938
|
}
|