expo-iap 5.3.0 → 5.3.2
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/{CLAUDE.md → AGENTS.md} +1 -3
- package/CONTRIBUTING.md +5 -12
- package/README.md +3 -3
- package/android/src/main/java/expo/modules/iap/ExpoIapHelper.kt +87 -39
- package/android/src/main/java/expo/modules/iap/ExpoIapLog.kt +4 -0
- package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +35 -27
- package/build/index.d.ts.map +1 -1
- package/build/index.js +65 -30
- package/build/index.js.map +1 -1
- package/build/index.kepler.d.ts.map +1 -1
- package/build/index.kepler.js +9 -1
- package/build/index.kepler.js.map +1 -1
- package/build/kit-api.d.ts +119 -2
- package/build/kit-api.d.ts.map +1 -1
- package/build/kit-api.js +3 -1
- package/build/kit-api.js.map +1 -1
- package/build/types.d.ts +33 -9
- package/build/types.d.ts.map +1 -1
- package/build/types.js.map +1 -1
- package/build/vega-adapter.d.ts.map +1 -1
- package/build/vega-adapter.js +5 -6
- package/build/vega-adapter.js.map +1 -1
- package/bun.lock +421 -1139
- package/ios/ExpoIapHelper.swift +80 -17
- package/ios/ExpoIapLog.swift +6 -6
- package/ios/ExpoIapModule.swift +20 -7
- package/ios/onside/OnsideIapModule.swift +28 -56
- package/openiap-versions.json +3 -3
- package/osv-scanner.toml +9 -0
- package/package.json +8 -2
- package/plugin/build/withVega.js +10 -0
- package/plugin/src/withVega.ts +10 -0
- package/src/index.kepler.ts +9 -1
- package/src/index.ts +54 -9
- package/src/kit-api.ts +125 -4
- package/src/types.ts +34 -9
- package/src/vega-adapter.ts +6 -11
- package/tests/ios/ExpoIapLogTests.swift +37 -0
package/ios/ExpoIapHelper.swift
CHANGED
|
@@ -40,6 +40,12 @@ enum ExpoIapHelper {
|
|
|
40
40
|
private static var purchaseUpdatedSub: OpenIAP.Subscription?
|
|
41
41
|
private static var purchaseUpdatedHandler: ((Purchase) -> Void)?
|
|
42
42
|
private static var purchaseUpdatedOptions = PurchaseUpdatedListenerOptions()
|
|
43
|
+
private static var listenerGeneration: UInt64 = 0
|
|
44
|
+
private static var activeListenerGeneration: UInt64?
|
|
45
|
+
private static var pendingConnectionCleanup: (
|
|
46
|
+
generation: UInt64,
|
|
47
|
+
task: Task<Void, Never>
|
|
48
|
+
)?
|
|
43
49
|
|
|
44
50
|
static func sanitizeDictionary(_ dictionary: [String: Any?]) -> [String: Any] {
|
|
45
51
|
var result: [String: Any] = [:]
|
|
@@ -91,9 +97,18 @@ enum ExpoIapHelper {
|
|
|
91
97
|
}
|
|
92
98
|
}
|
|
93
99
|
|
|
94
|
-
static func parseProductQueryType(_ rawValue:
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
static func parseProductQueryType(_ rawValue: Any?) throws -> ProductQueryType {
|
|
101
|
+
if rawValue == nil || rawValue is NSNull {
|
|
102
|
+
return .inApp
|
|
103
|
+
}
|
|
104
|
+
guard let stringValue = rawValue as? String else {
|
|
105
|
+
throw PurchaseError.make(
|
|
106
|
+
code: .developerError,
|
|
107
|
+
message: "Product type must be a string"
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
let raw = stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
111
|
+
if raw.isEmpty {
|
|
97
112
|
return .inApp
|
|
98
113
|
}
|
|
99
114
|
switch raw.lowercased() {
|
|
@@ -111,9 +126,26 @@ enum ExpoIapHelper {
|
|
|
111
126
|
}
|
|
112
127
|
}
|
|
113
128
|
|
|
129
|
+
static func parsePurchaseProductQueryType(_ rawValue: Any?) throws -> ProductQueryType {
|
|
130
|
+
if let rawValue, !(rawValue is String), !(rawValue is NSNull) {
|
|
131
|
+
throw PurchaseError.make(
|
|
132
|
+
code: .developerError,
|
|
133
|
+
message: "Purchase type must be a string"
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
let type = try parseProductQueryType(rawValue)
|
|
137
|
+
guard type != .all else {
|
|
138
|
+
throw PurchaseError.make(
|
|
139
|
+
code: .developerError,
|
|
140
|
+
message: "Product type all is only supported for product queries."
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
return type
|
|
144
|
+
}
|
|
145
|
+
|
|
114
146
|
static func decodeProductRequest(from payload: [String: Any]) throws -> ProductRequest {
|
|
115
147
|
if let skus = payload["skus"] as? [String], !skus.isEmpty {
|
|
116
|
-
let type = try parseProductQueryType(payload["type"]
|
|
148
|
+
let type = try parseProductQueryType(payload["type"])
|
|
117
149
|
return try OpenIapSerialization.productRequest(skus: skus, type: type)
|
|
118
150
|
}
|
|
119
151
|
|
|
@@ -137,6 +169,7 @@ enum ExpoIapHelper {
|
|
|
137
169
|
static func decodeRequestPurchaseProps(from payload: [String: Any]) throws
|
|
138
170
|
-> RequestPurchaseProps
|
|
139
171
|
{
|
|
172
|
+
let parsedType = try parsePurchaseProductQueryType(payload["type"])
|
|
140
173
|
if payload.keys.contains("requestPurchase"),
|
|
141
174
|
payload["requestPurchase"] != nil
|
|
142
175
|
{
|
|
@@ -151,10 +184,8 @@ enum ExpoIapHelper {
|
|
|
151
184
|
}
|
|
152
185
|
|
|
153
186
|
if payload.keys.contains("request"), let request = payload["request"] {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
var normalized: [String: Any] = ["type": purchaseType.rawValue]
|
|
157
|
-
switch purchaseType {
|
|
187
|
+
var normalized: [String: Any] = ["type": parsedType.rawValue]
|
|
188
|
+
switch parsedType {
|
|
158
189
|
case .subs:
|
|
159
190
|
normalized["requestSubscription"] = request
|
|
160
191
|
case .inApp:
|
|
@@ -170,17 +201,18 @@ enum ExpoIapHelper {
|
|
|
170
201
|
}
|
|
171
202
|
|
|
172
203
|
static func setupListeners(
|
|
173
|
-
module: ExpoIapModule,
|
|
174
204
|
purchaseUpdated: @escaping (Purchase) -> Void,
|
|
175
205
|
purchaseError: @escaping (PurchaseError) -> Void,
|
|
176
206
|
promotedProduct: @escaping (String) async -> Void,
|
|
177
207
|
subscriptionBillingIssue: @escaping (Purchase) -> Void
|
|
178
|
-
) {
|
|
208
|
+
) -> UInt64 {
|
|
179
209
|
listenerLock.lock()
|
|
180
210
|
defer { listenerLock.unlock() }
|
|
181
211
|
|
|
182
|
-
// Clean up any existing listeners first
|
|
183
212
|
cleanupListenersLocked()
|
|
213
|
+
listenerGeneration &+= 1
|
|
214
|
+
let generation = listenerGeneration
|
|
215
|
+
activeListenerGeneration = generation
|
|
184
216
|
|
|
185
217
|
purchaseUpdatedHandler = purchaseUpdated
|
|
186
218
|
attachPurchaseUpdatedListenerLocked()
|
|
@@ -208,6 +240,7 @@ enum ExpoIapHelper {
|
|
|
208
240
|
promotedProductSub,
|
|
209
241
|
billingIssueSub,
|
|
210
242
|
]
|
|
243
|
+
return generation
|
|
211
244
|
}
|
|
212
245
|
|
|
213
246
|
static func setPurchaseUpdatedListenerOptions(_ options: PurchaseUpdatedListenerOptions?) {
|
|
@@ -233,11 +266,37 @@ enum ExpoIapHelper {
|
|
|
233
266
|
}, options: purchaseUpdatedOptions)
|
|
234
267
|
}
|
|
235
268
|
|
|
236
|
-
static func
|
|
269
|
+
private static func beginStoreCleanup(
|
|
270
|
+
ifOwnedBy listenerGeneration: UInt64
|
|
271
|
+
) -> Task<Void, Never>? {
|
|
237
272
|
listenerLock.lock()
|
|
238
273
|
defer { listenerLock.unlock() }
|
|
239
274
|
|
|
275
|
+
guard activeListenerGeneration == listenerGeneration else { return nil }
|
|
240
276
|
cleanupListenersLocked()
|
|
277
|
+
activeListenerGeneration = nil
|
|
278
|
+
let previousTask = pendingConnectionCleanup?.task
|
|
279
|
+
let task = Task {
|
|
280
|
+
await previousTask?.value
|
|
281
|
+
_ = try? await OpenIapModule.shared.endConnection()
|
|
282
|
+
}
|
|
283
|
+
pendingConnectionCleanup = (listenerGeneration, task)
|
|
284
|
+
return task
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private static func pendingConnectionCleanupTask() -> Task<Void, Never>? {
|
|
288
|
+
listenerLock.lock()
|
|
289
|
+
defer { listenerLock.unlock() }
|
|
290
|
+
|
|
291
|
+
return pendingConnectionCleanup?.task
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private static func finishStoreCleanup(listenerGeneration: UInt64) {
|
|
295
|
+
listenerLock.lock()
|
|
296
|
+
defer { listenerLock.unlock() }
|
|
297
|
+
|
|
298
|
+
guard pendingConnectionCleanup?.generation == listenerGeneration else { return }
|
|
299
|
+
pendingConnectionCleanup = nil
|
|
241
300
|
}
|
|
242
301
|
|
|
243
302
|
private static func cleanupListenersLocked() {
|
|
@@ -253,9 +312,8 @@ enum ExpoIapHelper {
|
|
|
253
312
|
purchaseUpdatedOptions = PurchaseUpdatedListenerOptions()
|
|
254
313
|
}
|
|
255
314
|
|
|
256
|
-
static func setupStore(module: ExpoIapModule) {
|
|
315
|
+
static func setupStore(module: ExpoIapModule) -> UInt64 {
|
|
257
316
|
setupListeners(
|
|
258
|
-
module: module,
|
|
259
317
|
purchaseUpdated: { [weak module] purchase in
|
|
260
318
|
guard let module else { return }
|
|
261
319
|
let payload = sanitizeDictionary(OpenIapSerialization.purchase(purchase))
|
|
@@ -291,8 +349,13 @@ enum ExpoIapHelper {
|
|
|
291
349
|
)
|
|
292
350
|
}
|
|
293
351
|
|
|
294
|
-
static func cleanupStore() async {
|
|
295
|
-
|
|
296
|
-
|
|
352
|
+
static func cleanupStore(listenerGeneration: UInt64) async {
|
|
353
|
+
guard let task = beginStoreCleanup(ifOwnedBy: listenerGeneration) else { return }
|
|
354
|
+
await task.value
|
|
355
|
+
finishStoreCleanup(listenerGeneration: listenerGeneration)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
static func waitForStoreCleanup() async {
|
|
359
|
+
await pendingConnectionCleanupTask()?.value
|
|
297
360
|
}
|
|
298
361
|
}
|
package/ios/ExpoIapLog.swift
CHANGED
|
@@ -109,7 +109,7 @@ enum ExpoIapLog {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
private static func sanitize(_ value: Any?) -> Any? {
|
|
112
|
-
guard let value else { return nil }
|
|
112
|
+
guard let value, !(value is NSNull) else { return nil }
|
|
113
113
|
|
|
114
114
|
if let string = value as? String {
|
|
115
115
|
let trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
@@ -121,20 +121,20 @@ enum ExpoIapLog {
|
|
|
121
121
|
return sanitize(json) ?? string
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
if let dictionary = value as? [String: Any] {
|
|
125
|
-
return sanitizeDictionary(dictionary)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
124
|
if let optionalDictionary = value as? [String: Any?] {
|
|
129
125
|
var compact: [String: Any] = [:]
|
|
130
126
|
for (key, optionalValue) in optionalDictionary {
|
|
131
|
-
if let optionalValue {
|
|
127
|
+
if let optionalValue, !(optionalValue is NSNull) {
|
|
132
128
|
compact[key] = optionalValue
|
|
133
129
|
}
|
|
134
130
|
}
|
|
135
131
|
return sanitizeDictionary(compact)
|
|
136
132
|
}
|
|
137
133
|
|
|
134
|
+
if let dictionary = value as? [String: Any] {
|
|
135
|
+
return sanitizeDictionary(dictionary)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
138
|
if let array = value as? [Any] {
|
|
139
139
|
return array.compactMap { sanitize($0) }
|
|
140
140
|
}
|
package/ios/ExpoIapModule.swift
CHANGED
|
@@ -10,6 +10,7 @@ import UIKit
|
|
|
10
10
|
@MainActor
|
|
11
11
|
public final class ExpoIapModule: Module {
|
|
12
12
|
private var isInitialized = false
|
|
13
|
+
private var listenerGeneration: UInt64?
|
|
13
14
|
|
|
14
15
|
nonisolated public func definition() -> ModuleDefinition {
|
|
15
16
|
Name("ExpoIap")
|
|
@@ -25,21 +26,26 @@ public final class ExpoIapModule: Module {
|
|
|
25
26
|
IapEvent.subscriptionBillingIssue.rawValue
|
|
26
27
|
)
|
|
27
28
|
|
|
28
|
-
OnCreate {
|
|
29
|
-
Task { @MainActor in
|
|
30
|
-
|
|
29
|
+
OnCreate { [weak self] in
|
|
30
|
+
Task { @MainActor [weak self] in
|
|
31
|
+
guard let self else { return }
|
|
32
|
+
self.listenerGeneration = ExpoIapHelper.setupStore(module: self)
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
OnDestroy {
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
OnDestroy { [weak self] in
|
|
37
|
+
guard let self else { return }
|
|
38
|
+
Task { @MainActor [self] in
|
|
39
|
+
guard let listenerGeneration = self.listenerGeneration else { return }
|
|
40
|
+
self.listenerGeneration = nil
|
|
41
|
+
await ExpoIapHelper.cleanupStore(listenerGeneration: listenerGeneration)
|
|
37
42
|
}
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
AsyncFunction("initConnection") { (config: [String: Any]?) async throws -> Bool in
|
|
41
46
|
// Note: iOS doesn't support alternative billing config parameter
|
|
42
47
|
// Config is ignored on iOS platform
|
|
48
|
+
await ExpoIapHelper.waitForStoreCleanup()
|
|
43
49
|
let isConnected = try await OpenIapModule.shared.initConnection()
|
|
44
50
|
await MainActor.run { self.isInitialized = isConnected }
|
|
45
51
|
return isConnected
|
|
@@ -210,7 +216,14 @@ public final class ExpoIapModule: Module {
|
|
|
210
216
|
}
|
|
211
217
|
|
|
212
218
|
AsyncFunction("verifyPurchase") { (params: [String: Any]) async throws -> [String: Any] in
|
|
213
|
-
ExpoIapLog.payload(
|
|
219
|
+
ExpoIapLog.payload(
|
|
220
|
+
"verifyPurchase",
|
|
221
|
+
payload: [
|
|
222
|
+
"hasApple": params["apple"] != nil,
|
|
223
|
+
"hasGoogle": params["google"] != nil,
|
|
224
|
+
"hasHorizon": params["horizon"] != nil,
|
|
225
|
+
]
|
|
226
|
+
)
|
|
214
227
|
do {
|
|
215
228
|
let props = try OpenIapSerialization.verifyPurchaseProps(from: params)
|
|
216
229
|
let result = try await OpenIapModule.shared.verifyPurchase(props)
|
|
@@ -73,14 +73,15 @@ public final class ExpoIapOnsideModule: Module {
|
|
|
73
73
|
OnsideEvent.subscriptionBillingIssue.rawValue
|
|
74
74
|
)
|
|
75
75
|
|
|
76
|
-
OnCreate {
|
|
77
|
-
Task { @MainActor in
|
|
78
|
-
self
|
|
76
|
+
OnCreate { [weak self] in
|
|
77
|
+
Task { @MainActor [weak self] in
|
|
78
|
+
self?.configureObserverCallbacks()
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
OnDestroy {
|
|
83
|
-
|
|
82
|
+
OnDestroy { [weak self] in
|
|
83
|
+
guard let self else { return }
|
|
84
|
+
Task { @MainActor [self] in
|
|
84
85
|
self.cleanup()
|
|
85
86
|
}
|
|
86
87
|
}
|
|
@@ -104,6 +105,10 @@ public final class ExpoIapOnsideModule: Module {
|
|
|
104
105
|
|
|
105
106
|
AsyncFunction("fetchProducts") { (params: [String: Any]) async throws -> [[String: Any]] in
|
|
106
107
|
ExpoIapLog.payload("fetchProductsOnside", payload: params)
|
|
108
|
+
let request = try ExpoIapHelper.decodeProductRequest(from: params)
|
|
109
|
+
guard !request.skus.isEmpty else {
|
|
110
|
+
throw OnsideBridgeError.emptySkuList
|
|
111
|
+
}
|
|
107
112
|
try await ensureObserverRegistered()
|
|
108
113
|
|
|
109
114
|
var storefront = await Onside.defaultPaymentQueue().storefront
|
|
@@ -164,11 +169,6 @@ public final class ExpoIapOnsideModule: Module {
|
|
|
164
169
|
}
|
|
165
170
|
}
|
|
166
171
|
|
|
167
|
-
let request = try ExpoIapHelper.decodeProductRequest(from: params)
|
|
168
|
-
guard !request.skus.isEmpty else {
|
|
169
|
-
throw OnsideBridgeError.emptySkuList
|
|
170
|
-
}
|
|
171
|
-
|
|
172
172
|
let response = try await productFetcher.fetch(identifiers: Set(request.skus))
|
|
173
173
|
|
|
174
174
|
if !response.invalidProductIdentifiers.isEmpty {
|
|
@@ -198,14 +198,10 @@ public final class ExpoIapOnsideModule: Module {
|
|
|
198
198
|
AsyncFunction("requestPurchase") { (payload: [String: Any]) async throws -> Any? in
|
|
199
199
|
ExpoIapLog.payload("requestPurchaseOnside", payload: payload)
|
|
200
200
|
|
|
201
|
-
try
|
|
201
|
+
let purchaseRequest = try ExpoIapHelper.decodeRequestPurchaseProps(from: payload)
|
|
202
|
+
let sku = try resolveSku(from: purchaseRequest)
|
|
202
203
|
|
|
203
|
-
|
|
204
|
-
guard let s = resolveSku(from: payload) else {
|
|
205
|
-
throw OnsideBridgeError.emptySkuList
|
|
206
|
-
}
|
|
207
|
-
return s
|
|
208
|
-
}
|
|
204
|
+
try await ensureObserverRegistered()
|
|
209
205
|
|
|
210
206
|
try await ensureProductAvailable(sku: sku)
|
|
211
207
|
|
|
@@ -655,47 +651,23 @@ public final class ExpoIapOnsideModule: Module {
|
|
|
655
651
|
}
|
|
656
652
|
}
|
|
657
653
|
|
|
658
|
-
private func resolveSku(from
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
if
|
|
675
|
-
let requestPurchase = payload["requestPurchase"] as? [String: Any],
|
|
676
|
-
let apple = resolveAppleRequest(from: requestPurchase),
|
|
677
|
-
let sku = apple["sku"] as? String, !sku.isEmpty
|
|
678
|
-
{
|
|
679
|
-
return sku
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
if
|
|
683
|
-
let requestSubscription = payload["requestSubscription"] as? [String: Any],
|
|
684
|
-
let apple = resolveAppleRequest(from: requestSubscription),
|
|
685
|
-
let sku = apple["sku"] as? String, !sku.isEmpty
|
|
686
|
-
{
|
|
687
|
-
return sku
|
|
654
|
+
nonisolated private func resolveSku(from request: RequestPurchaseProps) throws -> String {
|
|
655
|
+
let sku: String?
|
|
656
|
+
switch (request.type, request.request) {
|
|
657
|
+
case (.inApp, .purchase(let platforms)):
|
|
658
|
+
sku = platforms.apple?.sku
|
|
659
|
+
case (.subs, .subscription(let platforms)):
|
|
660
|
+
sku = platforms.apple?.sku
|
|
661
|
+
default:
|
|
662
|
+
throw PurchaseError.make(
|
|
663
|
+
code: .developerError,
|
|
664
|
+
message: "Purchase type must match the request branch."
|
|
665
|
+
)
|
|
688
666
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
return first
|
|
667
|
+
guard let sku, !sku.isEmpty else {
|
|
668
|
+
throw OnsideBridgeError.emptySkuList
|
|
692
669
|
}
|
|
693
|
-
|
|
694
|
-
return nil
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
private func resolveAppleRequest(from request: [String: Any]) -> [String: Any]? {
|
|
698
|
-
request["apple"] as? [String: Any]
|
|
670
|
+
return sku
|
|
699
671
|
}
|
|
700
672
|
}
|
|
701
673
|
|
package/openiap-versions.json
CHANGED
package/osv-scanner.toml
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
[[IgnoredVulns]]
|
|
2
|
+
id = "GHSA-5p2g-fcmc-qvqq"
|
|
3
|
+
ignoreUntil = 2026-09-14
|
|
4
|
+
reason = "No patched image-size release exists; Metro uses it only during trusted local builds."
|
|
5
|
+
|
|
6
|
+
[[IgnoredVulns]]
|
|
7
|
+
id = "GHSA-w3rx-r6r6-pgpr"
|
|
8
|
+
ignoreUntil = 2026-09-14
|
|
9
|
+
reason = "No patched image-size release exists; Metro uses it only during trusted local builds."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-iap",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.2",
|
|
4
4
|
"description": "In App Purchase module in Expo",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -59,14 +59,17 @@
|
|
|
59
59
|
"eslint-config-expo": "^57.0.1",
|
|
60
60
|
"eslint-config-prettier": "^10.1.8",
|
|
61
61
|
"eslint-plugin-prettier": "^5.5.6",
|
|
62
|
+
"expo": "^57.0.12",
|
|
62
63
|
"expo-module-scripts": "^56.0.3",
|
|
63
64
|
"expo-modules-core": "^57.0.10",
|
|
64
65
|
"globals": "^17.9.0",
|
|
65
66
|
"husky": "^9.1.7",
|
|
66
67
|
"jest": "^29.7.0",
|
|
67
68
|
"react": "19.2.3",
|
|
69
|
+
"react-native": "0.86.2",
|
|
68
70
|
"test-renderer": "^1.2.0",
|
|
69
|
-
"ts-jest": "^29.4.12"
|
|
71
|
+
"ts-jest": "^29.4.12",
|
|
72
|
+
"typescript": "^5.9.2"
|
|
70
73
|
},
|
|
71
74
|
"peerDependencies": {
|
|
72
75
|
"@amazon-devices/keplerscript-appstore-iap-lib": "~2.13.0",
|
|
@@ -83,6 +86,9 @@
|
|
|
83
86
|
"optional": true
|
|
84
87
|
}
|
|
85
88
|
},
|
|
89
|
+
"overrides": {
|
|
90
|
+
"uuid": "11.1.1"
|
|
91
|
+
},
|
|
86
92
|
"expo": {
|
|
87
93
|
"plugin": "./app.plugin.js"
|
|
88
94
|
}
|
package/plugin/build/withVega.js
CHANGED
|
@@ -42,6 +42,9 @@ const GENERATED_JS_MARKER = '// @generated by expo-iap';
|
|
|
42
42
|
const DEFAULT_ICON_FILE = 'icon.png';
|
|
43
43
|
const DEFAULT_BUILD_TYPE = 'Release';
|
|
44
44
|
const DEFAULT_RUNTIME_MODULE = '/com.amazon.kepler.keplerscript.runtime.loader_2@IKeplerScript_2_0';
|
|
45
|
+
// Vega SDK 0.24 requires both the OS module and an [os.version] block.
|
|
46
|
+
const VEGA_OS_MODULE = '/com.amazon.vega.os@IVega_1_2';
|
|
47
|
+
const VEGA_OS_VERSION = '1.2';
|
|
45
48
|
const logOnce = (() => {
|
|
46
49
|
const printed = new Set();
|
|
47
50
|
return (msg) => {
|
|
@@ -133,6 +136,13 @@ id = "/com.amazon.iap.core@IIAPCoreUI"
|
|
|
133
136
|
[needs]
|
|
134
137
|
[[needs.module]]
|
|
135
138
|
id = "/com.amazon.kepler.appstore.iap.purchase.core@IAppstoreIAPPurchaseCoreService"
|
|
139
|
+
|
|
140
|
+
[[needs.module]]
|
|
141
|
+
id = ${escapeTomlString(VEGA_OS_MODULE)}
|
|
142
|
+
|
|
143
|
+
[os.version]
|
|
144
|
+
target = ${escapeTomlString(VEGA_OS_VERSION)}
|
|
145
|
+
min = ${escapeTomlString(VEGA_OS_VERSION)}
|
|
136
146
|
`;
|
|
137
147
|
exports.createVegaManifest = createVegaManifest;
|
|
138
148
|
const createVegaEntryPoint = () => `${GENERATED_JS_MARKER}
|
package/plugin/src/withVega.ts
CHANGED
|
@@ -13,6 +13,9 @@ const DEFAULT_ICON_FILE = 'icon.png';
|
|
|
13
13
|
const DEFAULT_BUILD_TYPE = 'Release';
|
|
14
14
|
const DEFAULT_RUNTIME_MODULE =
|
|
15
15
|
'/com.amazon.kepler.keplerscript.runtime.loader_2@IKeplerScript_2_0';
|
|
16
|
+
// Vega SDK 0.24 requires both the OS module and an [os.version] block.
|
|
17
|
+
const VEGA_OS_MODULE = '/com.amazon.vega.os@IVega_1_2';
|
|
18
|
+
const VEGA_OS_VERSION = '1.2';
|
|
16
19
|
|
|
17
20
|
const logOnce = (() => {
|
|
18
21
|
const printed = new Set<string>();
|
|
@@ -155,6 +158,13 @@ id = "/com.amazon.iap.core@IIAPCoreUI"
|
|
|
155
158
|
[needs]
|
|
156
159
|
[[needs.module]]
|
|
157
160
|
id = "/com.amazon.kepler.appstore.iap.purchase.core@IAppstoreIAPPurchaseCoreService"
|
|
161
|
+
|
|
162
|
+
[[needs.module]]
|
|
163
|
+
id = ${escapeTomlString(VEGA_OS_MODULE)}
|
|
164
|
+
|
|
165
|
+
[os.version]
|
|
166
|
+
target = ${escapeTomlString(VEGA_OS_VERSION)}
|
|
167
|
+
min = ${escapeTomlString(VEGA_OS_VERSION)}
|
|
158
168
|
`;
|
|
159
169
|
|
|
160
170
|
export const createVegaEntryPoint = (): string => `${GENERATED_JS_MARKER}
|
package/src/index.kepler.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
RequestSubscriptionPropsByPlatforms,
|
|
14
14
|
} from './types';
|
|
15
15
|
import {decodeAndroidPurchases} from './utils/availablePurchases';
|
|
16
|
+
import {createPurchaseError} from './utils/errorMapping';
|
|
16
17
|
|
|
17
18
|
export * from './types';
|
|
18
19
|
export * from './vega';
|
|
@@ -148,6 +149,13 @@ export const fetchProducts = async (
|
|
|
148
149
|
export const requestPurchase: MutationField<'requestPurchase'> = async (
|
|
149
150
|
args,
|
|
150
151
|
) => {
|
|
152
|
+
const type = normalizeProductType(args.type);
|
|
153
|
+
if (type === 'all') {
|
|
154
|
+
throw createPurchaseError({
|
|
155
|
+
code: ErrorCode.DeveloperError,
|
|
156
|
+
message: 'Product type all is only supported for product queries.',
|
|
157
|
+
});
|
|
158
|
+
}
|
|
151
159
|
const androidRequest = getAndroidRequest(args.request);
|
|
152
160
|
if (!androidRequest?.skus?.length) {
|
|
153
161
|
throw new Error(
|
|
@@ -158,7 +166,7 @@ export const requestPurchase: MutationField<'requestPurchase'> = async (
|
|
|
158
166
|
return normalizePurchaseArray(
|
|
159
167
|
await getModule().requestPurchase({
|
|
160
168
|
skus: androidRequest.skus,
|
|
161
|
-
type
|
|
169
|
+
type,
|
|
162
170
|
}),
|
|
163
171
|
);
|
|
164
172
|
};
|
package/src/index.ts
CHANGED
|
@@ -929,6 +929,31 @@ function normalizeRequestProps(
|
|
|
929
929
|
return request.google;
|
|
930
930
|
}
|
|
931
931
|
|
|
932
|
+
function validateAndroidPurchaseBranchOptions(
|
|
933
|
+
request: unknown,
|
|
934
|
+
type: ProductQueryType,
|
|
935
|
+
): void {
|
|
936
|
+
if (request == null || typeof request !== 'object') return;
|
|
937
|
+
const fields = request as Record<string, unknown>;
|
|
938
|
+
const subscriptionOnlyFields = [
|
|
939
|
+
'subscriptionOffers',
|
|
940
|
+
'subscriptionProductReplacementParams',
|
|
941
|
+
'purchaseToken',
|
|
942
|
+
'originalExternalTransactionId',
|
|
943
|
+
];
|
|
944
|
+
if (
|
|
945
|
+
(type === 'in-app' &&
|
|
946
|
+
subscriptionOnlyFields.some((field) => fields[field] != null)) ||
|
|
947
|
+
(type === 'subs' && fields.offerToken != null)
|
|
948
|
+
) {
|
|
949
|
+
throw createPurchaseError({
|
|
950
|
+
code: ErrorCode.DeveloperError,
|
|
951
|
+
message:
|
|
952
|
+
'Invalid request for Android. Purchase options must match the selected product type.',
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
932
957
|
/**
|
|
933
958
|
* Initiate a purchase or subscription flow. The result is delivered through
|
|
934
959
|
* `purchaseUpdatedListener` — NOT the return value.
|
|
@@ -960,14 +985,21 @@ export const requestPurchase: MutationField<'requestPurchase'> = async (
|
|
|
960
985
|
) => {
|
|
961
986
|
const {request, type} = args;
|
|
962
987
|
const {canonical, native} = normalizeProductType(type as ProductTypeInput);
|
|
988
|
+
if (canonical === 'all') {
|
|
989
|
+
throw createPurchaseError({
|
|
990
|
+
code: ErrorCode.DeveloperError,
|
|
991
|
+
message: 'Product type all is only supported for product queries.',
|
|
992
|
+
});
|
|
993
|
+
}
|
|
963
994
|
const isInAppPurchase = canonical === 'in-app';
|
|
964
995
|
|
|
965
996
|
if (Platform.OS === 'ios') {
|
|
966
997
|
const normalizedRequest = normalizeRequestProps(request, 'ios');
|
|
967
998
|
|
|
968
999
|
if (!normalizedRequest?.sku) {
|
|
969
|
-
throw
|
|
970
|
-
|
|
1000
|
+
throw createPurchaseError({
|
|
1001
|
+
message:
|
|
1002
|
+
'Invalid request for Apple. The `sku` property is required and must be a string.\n\n' +
|
|
971
1003
|
'Expected format:\n' +
|
|
972
1004
|
' requestPurchase({\n' +
|
|
973
1005
|
' request: {\n' +
|
|
@@ -977,7 +1009,8 @@ export const requestPurchase: MutationField<'requestPurchase'> = async (
|
|
|
977
1009
|
' type: "in-app"\n' +
|
|
978
1010
|
' })\n\n' +
|
|
979
1011
|
'See: https://openiap.dev/docs/apis/request-purchase',
|
|
980
|
-
|
|
1012
|
+
code: ErrorCode.EmptySkuList,
|
|
1013
|
+
});
|
|
981
1014
|
}
|
|
982
1015
|
|
|
983
1016
|
if (canonical !== 'in-app' && canonical !== 'subs') {
|
|
@@ -1013,6 +1046,14 @@ export const requestPurchase: MutationField<'requestPurchase'> = async (
|
|
|
1013
1046
|
}
|
|
1014
1047
|
|
|
1015
1048
|
if (isAndroidStoreRuntime()) {
|
|
1049
|
+
validateAndroidPurchaseBranchOptions(
|
|
1050
|
+
(
|
|
1051
|
+
request as
|
|
1052
|
+
| RequestPurchasePropsByPlatforms
|
|
1053
|
+
| RequestSubscriptionPropsByPlatforms
|
|
1054
|
+
)?.google,
|
|
1055
|
+
canonical,
|
|
1056
|
+
);
|
|
1016
1057
|
if (isInAppPurchase) {
|
|
1017
1058
|
const normalizedRequest = normalizeRequestProps(
|
|
1018
1059
|
request as RequestPurchasePropsByPlatforms,
|
|
@@ -1020,8 +1061,9 @@ export const requestPurchase: MutationField<'requestPurchase'> = async (
|
|
|
1020
1061
|
) as RequestPurchaseAndroidProps | null | undefined;
|
|
1021
1062
|
|
|
1022
1063
|
if (!normalizedRequest?.skus?.length) {
|
|
1023
|
-
throw
|
|
1024
|
-
|
|
1064
|
+
throw createPurchaseError({
|
|
1065
|
+
message:
|
|
1066
|
+
'Invalid request for Google. The `skus` property is required and must be a non-empty array.\n\n' +
|
|
1025
1067
|
'Expected format:\n' +
|
|
1026
1068
|
' requestPurchase({\n' +
|
|
1027
1069
|
' request: {\n' +
|
|
@@ -1031,7 +1073,8 @@ export const requestPurchase: MutationField<'requestPurchase'> = async (
|
|
|
1031
1073
|
' type: "in-app"\n' +
|
|
1032
1074
|
' })\n\n' +
|
|
1033
1075
|
'See: https://openiap.dev/docs/apis/request-purchase',
|
|
1034
|
-
|
|
1076
|
+
code: ErrorCode.EmptySkuList,
|
|
1077
|
+
});
|
|
1035
1078
|
}
|
|
1036
1079
|
|
|
1037
1080
|
const {
|
|
@@ -1075,8 +1118,9 @@ export const requestPurchase: MutationField<'requestPurchase'> = async (
|
|
|
1075
1118
|
) as RequestSubscriptionAndroidProps | null | undefined;
|
|
1076
1119
|
|
|
1077
1120
|
if (!normalizedRequest?.skus?.length) {
|
|
1078
|
-
throw
|
|
1079
|
-
|
|
1121
|
+
throw createPurchaseError({
|
|
1122
|
+
message:
|
|
1123
|
+
'Invalid request for Google. The `skus` property is required and must be a non-empty array.\n\n' +
|
|
1080
1124
|
'Expected format:\n' +
|
|
1081
1125
|
' requestPurchase({\n' +
|
|
1082
1126
|
' request: {\n' +
|
|
@@ -1086,7 +1130,8 @@ export const requestPurchase: MutationField<'requestPurchase'> = async (
|
|
|
1086
1130
|
' type: "subs"\n' +
|
|
1087
1131
|
' })\n\n' +
|
|
1088
1132
|
'See: https://openiap.dev/docs/apis/request-purchase',
|
|
1089
|
-
|
|
1133
|
+
code: ErrorCode.EmptySkuList,
|
|
1134
|
+
});
|
|
1090
1135
|
}
|
|
1091
1136
|
|
|
1092
1137
|
const {
|