@capgo/native-purchases 8.0.4 → 8.0.6
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/android/build.gradle +1 -1
- package/android/src/main/java/ee/forgr/nativepurchases/NativePurchasesPlugin.java +1 -1
- package/ios/Sources/NativePurchasesPlugin/NativePurchasesPlugin.swift +200 -304
- package/ios/Sources/NativePurchasesPlugin/Product+CapacitorPurchasesPlugin.swift +0 -1
- package/ios/Sources/NativePurchasesPlugin/TransactionHelpers.swift +85 -129
- package/package.json +1 -1
package/android/build.gradle
CHANGED
|
@@ -50,7 +50,7 @@ repositories {
|
|
|
50
50
|
|
|
51
51
|
dependencies {
|
|
52
52
|
implementation "com.google.guava:guava:33.5.0-android"
|
|
53
|
-
def billing_version = "8.2.
|
|
53
|
+
def billing_version = "8.2.1"
|
|
54
54
|
implementation "com.android.billingclient:billing:$billing_version"
|
|
55
55
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
56
56
|
implementation project(':capacitor-android')
|
|
@@ -42,7 +42,7 @@ import org.json.JSONArray;
|
|
|
42
42
|
@CapacitorPlugin(name = "NativePurchases")
|
|
43
43
|
public class NativePurchasesPlugin extends Plugin {
|
|
44
44
|
|
|
45
|
-
private final String pluginVersion = "8.0.
|
|
45
|
+
private final String pluginVersion = "8.0.6";
|
|
46
46
|
public static final String TAG = "NativePurchases";
|
|
47
47
|
private static final Phaser semaphoreReady = new Phaser(1);
|
|
48
48
|
private BillingClient billingClient;
|
|
@@ -2,10 +2,6 @@ import Foundation
|
|
|
2
2
|
import Capacitor
|
|
3
3
|
import StoreKit
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* Please read the Capacitor iOS Plugin Development Guide
|
|
7
|
-
* here: https://capacitorjs.com/docs/plugins/ios
|
|
8
|
-
*/
|
|
9
5
|
@objc(NativePurchasesPlugin)
|
|
10
6
|
public class NativePurchasesPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
11
7
|
public let identifier = "NativePurchasesPlugin"
|
|
@@ -24,7 +20,7 @@ public class NativePurchasesPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
24
20
|
CAPPluginMethod(name: "isEntitledToOldBusinessModel", returnType: CAPPluginReturnPromise)
|
|
25
21
|
]
|
|
26
22
|
|
|
27
|
-
private let pluginVersion: String = "8.0.
|
|
23
|
+
private let pluginVersion: String = "8.0.6"
|
|
28
24
|
private var transactionUpdatesTask: Task<Void, Never>?
|
|
29
25
|
|
|
30
26
|
@objc func getPluginVersion(_ call: CAPPluginCall) {
|
|
@@ -33,38 +29,28 @@ public class NativePurchasesPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
33
29
|
|
|
34
30
|
override public func load() {
|
|
35
31
|
super.load()
|
|
36
|
-
|
|
37
|
-
if #available(iOS 15.0, *) {
|
|
38
|
-
startTransactionUpdatesListener()
|
|
39
|
-
}
|
|
32
|
+
startTransactionUpdatesListener()
|
|
40
33
|
}
|
|
41
34
|
|
|
42
35
|
deinit {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
private func cancelTransactionUpdatesListener() {
|
|
47
|
-
self.transactionUpdatesTask?.cancel()
|
|
48
|
-
self.transactionUpdatesTask = nil
|
|
36
|
+
transactionUpdatesTask?.cancel()
|
|
37
|
+
transactionUpdatesTask = nil
|
|
49
38
|
}
|
|
50
39
|
|
|
51
|
-
@available(iOS 15.0, *)
|
|
52
40
|
private func startTransactionUpdatesListener() {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
let task = Task.detached { [weak self] in
|
|
41
|
+
transactionUpdatesTask?.cancel()
|
|
42
|
+
transactionUpdatesTask = Task.detached { [weak self] in
|
|
56
43
|
for await result in Transaction.updates {
|
|
57
44
|
guard !Task.isCancelled else { break }
|
|
58
45
|
switch result {
|
|
59
46
|
case .verified(let transaction):
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
47
|
+
let payload = await TransactionHelpers.buildTransactionResponse(
|
|
48
|
+
from: transaction,
|
|
49
|
+
jwsRepresentation: result.jwsRepresentation,
|
|
50
|
+
alwaysIncludeWillCancel: true
|
|
51
|
+
)
|
|
64
52
|
await transaction.finish()
|
|
65
|
-
|
|
66
|
-
// Notify JS listeners on main thread, after slight delay
|
|
67
|
-
try? await Task.sleep(nanoseconds: 500_000_000) // 0.5s delay
|
|
53
|
+
try? await Task.sleep(nanoseconds: 500_000_000)
|
|
68
54
|
await MainActor.run {
|
|
69
55
|
self?.notifyListeners("transactionUpdated", data: payload)
|
|
70
56
|
}
|
|
@@ -78,93 +64,70 @@ public class NativePurchasesPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
78
64
|
}
|
|
79
65
|
}
|
|
80
66
|
}
|
|
81
|
-
transactionUpdatesTask = task
|
|
82
67
|
}
|
|
83
68
|
|
|
84
|
-
// MARK: - Plugin Methods
|
|
85
|
-
|
|
86
69
|
@objc func isBillingSupported(_ call: CAPPluginCall) {
|
|
87
|
-
|
|
88
|
-
call.resolve([
|
|
89
|
-
"isBillingSupported": true
|
|
90
|
-
])
|
|
91
|
-
} else {
|
|
92
|
-
call.resolve([
|
|
93
|
-
"isBillingSupported": false
|
|
94
|
-
])
|
|
95
|
-
}
|
|
70
|
+
call.resolve(["isBillingSupported": true])
|
|
96
71
|
}
|
|
97
72
|
|
|
98
73
|
@objc func purchaseProduct(_ call: CAPPluginCall) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
print("Auto-acknowledge enabled: \(autoAcknowledge)")
|
|
112
|
-
|
|
113
|
-
Task { @MainActor in
|
|
114
|
-
do {
|
|
115
|
-
let products = try await Product.products(for: [productIdentifier])
|
|
116
|
-
guard let product = products.first else {
|
|
117
|
-
call.reject("Cannot find product for id \(productIdentifier)")
|
|
118
|
-
return
|
|
119
|
-
}
|
|
74
|
+
print("purchaseProduct")
|
|
75
|
+
let productIdentifier = call.getString("productIdentifier", "")
|
|
76
|
+
let quantity = call.getInt("quantity", 1)
|
|
77
|
+
let appAccountToken = call.getString("appAccountToken")
|
|
78
|
+
let autoAcknowledge = call.getBool("autoAcknowledgePurchases") ?? true
|
|
79
|
+
|
|
80
|
+
if productIdentifier.isEmpty {
|
|
81
|
+
call.reject("productIdentifier is Empty, give an id")
|
|
82
|
+
return
|
|
83
|
+
}
|
|
120
84
|
|
|
121
|
-
|
|
122
|
-
let result = try await product.purchase(options: purchaseOptions)
|
|
123
|
-
print("purchaseProduct result \(result)")
|
|
85
|
+
print("Auto-acknowledge enabled: \(autoAcknowledge)")
|
|
124
86
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
87
|
+
Task { @MainActor in
|
|
88
|
+
do {
|
|
89
|
+
let products = try await Product.products(for: [productIdentifier])
|
|
90
|
+
guard let product = products.first else {
|
|
91
|
+
call.reject("Cannot find product for id \(productIdentifier)")
|
|
92
|
+
return
|
|
129
93
|
}
|
|
130
|
-
}
|
|
131
|
-
} else {
|
|
132
|
-
print("Not implemented under ios 15")
|
|
133
|
-
call.reject("Not implemented under ios 15")
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
94
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
95
|
+
var purchaseOptions = Set<Product.PurchaseOption>()
|
|
96
|
+
purchaseOptions.insert(.quantity(quantity))
|
|
97
|
+
if let token = appAccountToken, !token.isEmpty, let uuid = UUID(uuidString: token) {
|
|
98
|
+
purchaseOptions.insert(.appAccountToken(uuid))
|
|
99
|
+
}
|
|
141
100
|
|
|
142
|
-
|
|
143
|
-
|
|
101
|
+
let result = try await product.purchase(options: purchaseOptions)
|
|
102
|
+
print("purchaseProduct result \(result)")
|
|
103
|
+
await self.handlePurchaseResult(result, call: call, autoFinish: autoAcknowledge)
|
|
104
|
+
} catch {
|
|
105
|
+
print(error)
|
|
106
|
+
call.reject(error.localizedDescription)
|
|
107
|
+
}
|
|
144
108
|
}
|
|
145
|
-
|
|
146
|
-
return purchaseOptions
|
|
147
109
|
}
|
|
148
110
|
|
|
149
|
-
@available(iOS 15.0, *)
|
|
150
111
|
@MainActor
|
|
151
|
-
private func handlePurchaseResult(
|
|
112
|
+
private func handlePurchaseResult(
|
|
113
|
+
_ result: Product.PurchaseResult,
|
|
114
|
+
call: CAPPluginCall,
|
|
115
|
+
autoFinish: Bool
|
|
116
|
+
) async {
|
|
152
117
|
switch result {
|
|
153
118
|
case let .success(verificationResult):
|
|
154
119
|
switch verificationResult {
|
|
155
120
|
case .verified(let transaction):
|
|
156
|
-
let response = await TransactionHelpers.buildTransactionResponse(
|
|
157
|
-
|
|
121
|
+
let response = await TransactionHelpers.buildTransactionResponse(
|
|
122
|
+
from: transaction,
|
|
123
|
+
jwsRepresentation: verificationResult.jwsRepresentation
|
|
124
|
+
)
|
|
158
125
|
if autoFinish {
|
|
159
126
|
print("Auto-finishing transaction: \(transaction.id)")
|
|
160
127
|
await transaction.finish()
|
|
161
128
|
} else {
|
|
162
129
|
print("Manual finish required for transaction: \(transaction.id)")
|
|
163
|
-
print("Transaction will remain unfinished until acknowledgePurchase() is called")
|
|
164
|
-
// Don't finish - transaction remains in StoreKit's queue
|
|
165
|
-
// Can be retrieved later via Transaction.all
|
|
166
130
|
}
|
|
167
|
-
|
|
168
131
|
call.resolve(response)
|
|
169
132
|
case .unverified(_, let error):
|
|
170
133
|
call.reject(error.localizedDescription)
|
|
@@ -179,237 +142,136 @@ public class NativePurchasesPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
179
142
|
}
|
|
180
143
|
|
|
181
144
|
@objc func restorePurchases(_ call: CAPPluginCall) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
for transaction in SKPaymentQueue.default().transactions {
|
|
189
|
-
SKPaymentQueue.default().finishTransaction(transaction)
|
|
190
|
-
}
|
|
191
|
-
await MainActor.run {
|
|
192
|
-
call.resolve()
|
|
193
|
-
}
|
|
194
|
-
} catch {
|
|
195
|
-
await MainActor.run {
|
|
196
|
-
call.reject(error.localizedDescription)
|
|
197
|
-
}
|
|
145
|
+
print("restorePurchases")
|
|
146
|
+
Task {
|
|
147
|
+
do {
|
|
148
|
+
try await AppStore.sync()
|
|
149
|
+
for transaction in SKPaymentQueue.default().transactions {
|
|
150
|
+
SKPaymentQueue.default().finishTransaction(transaction)
|
|
198
151
|
}
|
|
152
|
+
await MainActor.run { call.resolve() }
|
|
153
|
+
} catch {
|
|
154
|
+
await MainActor.run { call.reject(error.localizedDescription) }
|
|
199
155
|
}
|
|
200
|
-
} else {
|
|
201
|
-
print("Not implemented under ios 15")
|
|
202
|
-
call.reject("Not implemented under ios 15")
|
|
203
156
|
}
|
|
204
157
|
}
|
|
205
158
|
|
|
206
159
|
@objc func getProducts(_ call: CAPPluginCall) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
])
|
|
219
|
-
}
|
|
220
|
-
} catch {
|
|
221
|
-
print("error \(error)")
|
|
222
|
-
await MainActor.run {
|
|
223
|
-
call.reject(error.localizedDescription)
|
|
224
|
-
}
|
|
225
|
-
}
|
|
160
|
+
let productIdentifiers = call.getArray("productIdentifiers", String.self) ?? []
|
|
161
|
+
print("productIdentifiers \(productIdentifiers)")
|
|
162
|
+
Task {
|
|
163
|
+
do {
|
|
164
|
+
let products = try await Product.products(for: productIdentifiers)
|
|
165
|
+
print("products \(products)")
|
|
166
|
+
let productsJson: [[String: Any]] = products.map { $0.dictionary }
|
|
167
|
+
await MainActor.run { call.resolve(["products": productsJson]) }
|
|
168
|
+
} catch {
|
|
169
|
+
print("error \(error)")
|
|
170
|
+
await MainActor.run { call.reject(error.localizedDescription) }
|
|
226
171
|
}
|
|
227
|
-
} else {
|
|
228
|
-
print("Not implemented under ios 15")
|
|
229
|
-
call.reject("Not implemented under ios 15")
|
|
230
172
|
}
|
|
231
173
|
}
|
|
232
174
|
|
|
233
175
|
@objc func getProduct(_ call: CAPPluginCall) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
176
|
+
let productIdentifier = call.getString("productIdentifier") ?? ""
|
|
177
|
+
print("productIdentifier \(productIdentifier)")
|
|
178
|
+
if productIdentifier.isEmpty {
|
|
179
|
+
call.reject("productIdentifier is empty")
|
|
180
|
+
return
|
|
181
|
+
}
|
|
241
182
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
} else {
|
|
252
|
-
await MainActor.run {
|
|
253
|
-
call.reject("Product not found")
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
} catch {
|
|
257
|
-
print(error)
|
|
258
|
-
await MainActor.run {
|
|
259
|
-
call.reject(error.localizedDescription)
|
|
260
|
-
}
|
|
183
|
+
Task {
|
|
184
|
+
do {
|
|
185
|
+
let products = try await Product.products(for: [productIdentifier])
|
|
186
|
+
print("products \(products)")
|
|
187
|
+
if let product = products.first {
|
|
188
|
+
await MainActor.run { call.resolve(["product": product.dictionary]) }
|
|
189
|
+
} else {
|
|
190
|
+
await MainActor.run { call.reject("Product not found") }
|
|
261
191
|
}
|
|
192
|
+
} catch {
|
|
193
|
+
print(error)
|
|
194
|
+
await MainActor.run { call.reject(error.localizedDescription) }
|
|
262
195
|
}
|
|
263
|
-
} else {
|
|
264
|
-
print("Not implemented under iOS 15")
|
|
265
|
-
call.reject("Not implemented under iOS 15")
|
|
266
196
|
}
|
|
267
197
|
}
|
|
268
198
|
|
|
269
199
|
@objc func getPurchases(_ call: CAPPluginCall) {
|
|
200
|
+
print("getPurchases")
|
|
270
201
|
let appAccountTokenFilter = call.getString("appAccountToken")
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
call.resolve(["purchases": allPurchases])
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
} else {
|
|
280
|
-
print("Not implemented under iOS 15")
|
|
281
|
-
call.reject("Not implemented under iOS 15")
|
|
202
|
+
Task {
|
|
203
|
+
let allPurchases = await TransactionHelpers.collectAllPurchases(
|
|
204
|
+
appAccountTokenFilter: appAccountTokenFilter
|
|
205
|
+
)
|
|
206
|
+
await MainActor.run { call.resolve(["purchases": allPurchases]) }
|
|
282
207
|
}
|
|
283
208
|
}
|
|
284
209
|
|
|
285
210
|
@objc func manageSubscriptions(_ call: CAPPluginCall) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
call.reject("Unable to get window scene")
|
|
293
|
-
return
|
|
294
|
-
}
|
|
295
|
-
// Open the App Store subscription management page
|
|
296
|
-
try await AppStore.showManageSubscriptions(in: windowScene)
|
|
297
|
-
call.resolve()
|
|
298
|
-
} catch {
|
|
299
|
-
print("manageSubscriptions error: \(error)")
|
|
300
|
-
call.reject(error.localizedDescription)
|
|
211
|
+
print("manageSubscriptions")
|
|
212
|
+
Task { @MainActor in
|
|
213
|
+
do {
|
|
214
|
+
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
|
|
215
|
+
call.reject("Unable to get window scene")
|
|
216
|
+
return
|
|
301
217
|
}
|
|
218
|
+
try await AppStore.showManageSubscriptions(in: windowScene)
|
|
219
|
+
call.resolve()
|
|
220
|
+
} catch {
|
|
221
|
+
print("manageSubscriptions error: \(error)")
|
|
222
|
+
call.reject(error.localizedDescription)
|
|
302
223
|
}
|
|
303
|
-
} else {
|
|
304
|
-
print("Not implemented under iOS 15")
|
|
305
|
-
call.reject("Not implemented under iOS 15")
|
|
306
224
|
}
|
|
307
225
|
}
|
|
308
226
|
|
|
309
227
|
@objc func acknowledgePurchase(_ call: CAPPluginCall) {
|
|
310
|
-
|
|
311
|
-
print("acknowledgePurchase called on iOS")
|
|
228
|
+
print("acknowledgePurchase called on iOS")
|
|
312
229
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
// On iOS, purchaseToken is the transactionId (UInt64 as string)
|
|
319
|
-
guard let transactionId = UInt64(purchaseToken) else {
|
|
320
|
-
call.reject("Invalid purchaseToken format")
|
|
321
|
-
return
|
|
322
|
-
}
|
|
230
|
+
guard let purchaseToken = call.getString("purchaseToken") else {
|
|
231
|
+
call.reject("purchaseToken is required")
|
|
232
|
+
return
|
|
233
|
+
}
|
|
323
234
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
for await verificationResult in Transaction.all {
|
|
330
|
-
switch verificationResult {
|
|
331
|
-
case .verified(let transaction):
|
|
332
|
-
if transaction.id == transactionId {
|
|
333
|
-
foundTransaction = transaction
|
|
334
|
-
break
|
|
335
|
-
}
|
|
336
|
-
case .unverified:
|
|
337
|
-
continue
|
|
338
|
-
}
|
|
339
|
-
if foundTransaction != nil {
|
|
340
|
-
break
|
|
341
|
-
}
|
|
342
|
-
}
|
|
235
|
+
guard let transactionId = UInt64(purchaseToken) else {
|
|
236
|
+
call.reject("Invalid purchaseToken format")
|
|
237
|
+
return
|
|
238
|
+
}
|
|
343
239
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
240
|
+
Task {
|
|
241
|
+
var foundTransaction: Transaction?
|
|
242
|
+
for await verificationResult in Transaction.all {
|
|
243
|
+
if case .verified(let transaction) = verificationResult, transaction.id == transactionId {
|
|
244
|
+
foundTransaction = transaction
|
|
245
|
+
break
|
|
349
246
|
}
|
|
247
|
+
}
|
|
350
248
|
|
|
351
|
-
|
|
352
|
-
await transaction.finish()
|
|
353
|
-
|
|
249
|
+
guard let transaction = foundTransaction else {
|
|
354
250
|
await MainActor.run {
|
|
355
|
-
|
|
356
|
-
call.resolve()
|
|
251
|
+
call.reject("Transaction not found or already finished. Transaction ID: \(transactionId)")
|
|
357
252
|
}
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
print("Manually finishing transaction: \(transaction.id)")
|
|
257
|
+
await transaction.finish()
|
|
258
|
+
await MainActor.run {
|
|
259
|
+
print("Transaction finished successfully")
|
|
260
|
+
call.resolve()
|
|
358
261
|
}
|
|
359
|
-
} else {
|
|
360
|
-
call.reject("Not implemented under iOS 15")
|
|
361
262
|
}
|
|
362
263
|
}
|
|
363
264
|
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// MARK: - iOS 16+ App Transaction Methods
|
|
268
|
+
extension NativePurchasesPlugin {
|
|
364
269
|
@objc func getAppTransaction(_ call: CAPPluginCall) {
|
|
365
270
|
if #available(iOS 16.0, *) {
|
|
366
|
-
print("getAppTransaction called on iOS")
|
|
367
271
|
Task { @MainActor in
|
|
368
|
-
|
|
369
|
-
let verificationResult = try await AppTransaction.shared
|
|
370
|
-
switch verificationResult {
|
|
371
|
-
case .verified(let appTransaction):
|
|
372
|
-
var response: [String: Any] = [:]
|
|
373
|
-
|
|
374
|
-
// originalAppVersion is the CFBundleVersion (build number) at the time of original download
|
|
375
|
-
response["originalAppVersion"] = appTransaction.originalAppVersion
|
|
376
|
-
|
|
377
|
-
// Original purchase date
|
|
378
|
-
response["originalPurchaseDate"] = ISO8601DateFormatter().string(from: appTransaction.originalPurchaseDate)
|
|
379
|
-
|
|
380
|
-
// Bundle ID
|
|
381
|
-
response["bundleId"] = appTransaction.bundleID
|
|
382
|
-
|
|
383
|
-
// Current app version (build number)
|
|
384
|
-
response["appVersion"] = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? ""
|
|
385
|
-
|
|
386
|
-
// Environment
|
|
387
|
-
switch appTransaction.environment {
|
|
388
|
-
case .sandbox:
|
|
389
|
-
response["environment"] = "Sandbox"
|
|
390
|
-
case .production:
|
|
391
|
-
response["environment"] = "Production"
|
|
392
|
-
case .xcode:
|
|
393
|
-
response["environment"] = "Xcode"
|
|
394
|
-
default:
|
|
395
|
-
response["environment"] = "Production"
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// JWS representation for server-side verification
|
|
399
|
-
response["jwsRepresentation"] = verificationResult.jwsRepresentation
|
|
400
|
-
|
|
401
|
-
call.resolve(["appTransaction": response])
|
|
402
|
-
|
|
403
|
-
case .unverified(_, let error):
|
|
404
|
-
call.reject("App transaction verification failed: \(error.localizedDescription)")
|
|
405
|
-
}
|
|
406
|
-
} catch {
|
|
407
|
-
print("getAppTransaction error: \(error)")
|
|
408
|
-
call.reject("Failed to get app transaction: \(error.localizedDescription)")
|
|
409
|
-
}
|
|
272
|
+
await self.handleGetAppTransaction(call)
|
|
410
273
|
}
|
|
411
274
|
} else {
|
|
412
|
-
print("getAppTransaction not implemented under iOS 16")
|
|
413
275
|
call.reject("App Transaction requires iOS 16.0 or later")
|
|
414
276
|
}
|
|
415
277
|
}
|
|
@@ -421,44 +283,78 @@ public class NativePurchasesPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
421
283
|
}
|
|
422
284
|
|
|
423
285
|
if #available(iOS 16.0, *) {
|
|
424
|
-
print("isEntitledToOldBusinessModel called with targetBuildNumber: \(targetBuildNumber)")
|
|
425
286
|
Task { @MainActor in
|
|
426
|
-
|
|
427
|
-
let verificationResult = try await AppTransaction.shared
|
|
428
|
-
switch verificationResult {
|
|
429
|
-
case .verified(let appTransaction):
|
|
430
|
-
let originalBuildNumber = appTransaction.originalAppVersion
|
|
431
|
-
|
|
432
|
-
// Compare build numbers (integers)
|
|
433
|
-
let isOlder = self.compareVersions(originalBuildNumber, targetBuildNumber) < 0
|
|
434
|
-
|
|
435
|
-
call.resolve([
|
|
436
|
-
"isOlderVersion": isOlder,
|
|
437
|
-
"originalAppVersion": originalBuildNumber
|
|
438
|
-
])
|
|
439
|
-
|
|
440
|
-
case .unverified(_, let error):
|
|
441
|
-
call.reject("App transaction verification failed: \(error.localizedDescription)")
|
|
442
|
-
}
|
|
443
|
-
} catch {
|
|
444
|
-
print("isEntitledToOldBusinessModel error: \(error)")
|
|
445
|
-
call.reject("Failed to get app transaction: \(error.localizedDescription)")
|
|
446
|
-
}
|
|
287
|
+
await self.handleIsEntitledToOldBusinessModel(call, targetBuildNumber: targetBuildNumber)
|
|
447
288
|
}
|
|
448
289
|
} else {
|
|
449
|
-
print("isEntitledToOldBusinessModel not implemented under iOS 16")
|
|
450
290
|
call.reject("App Transaction requires iOS 16.0 or later")
|
|
451
291
|
}
|
|
452
292
|
}
|
|
453
293
|
|
|
454
|
-
|
|
294
|
+
@available(iOS 16.0, *)
|
|
295
|
+
@MainActor
|
|
296
|
+
private func handleGetAppTransaction(_ call: CAPPluginCall) async {
|
|
297
|
+
print("getAppTransaction called on iOS")
|
|
298
|
+
do {
|
|
299
|
+
let verificationResult = try await AppTransaction.shared
|
|
300
|
+
switch verificationResult {
|
|
301
|
+
case .verified(let appTransaction):
|
|
302
|
+
let response: [String: Any] = [
|
|
303
|
+
"originalAppVersion": appTransaction.originalAppVersion,
|
|
304
|
+
"originalPurchaseDate": ISO8601DateFormatter().string(
|
|
305
|
+
from: appTransaction.originalPurchaseDate
|
|
306
|
+
),
|
|
307
|
+
"bundleId": appTransaction.bundleID,
|
|
308
|
+
"appVersion": Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "",
|
|
309
|
+
"jwsRepresentation": verificationResult.jwsRepresentation,
|
|
310
|
+
"environment": appTransaction.environment.environmentString
|
|
311
|
+
]
|
|
312
|
+
call.resolve(["appTransaction": response])
|
|
313
|
+
case .unverified(_, let error):
|
|
314
|
+
call.reject("App transaction verification failed: \(error.localizedDescription)")
|
|
315
|
+
}
|
|
316
|
+
} catch {
|
|
317
|
+
print("getAppTransaction error: \(error)")
|
|
318
|
+
call.reject("Failed to get app transaction: \(error.localizedDescription)")
|
|
319
|
+
}
|
|
320
|
+
}
|
|
455
321
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
private func
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
322
|
+
@available(iOS 16.0, *)
|
|
323
|
+
@MainActor
|
|
324
|
+
private func handleIsEntitledToOldBusinessModel(
|
|
325
|
+
_ call: CAPPluginCall,
|
|
326
|
+
targetBuildNumber: String
|
|
327
|
+
) async {
|
|
328
|
+
print("isEntitledToOldBusinessModel called with targetBuildNumber: \(targetBuildNumber)")
|
|
329
|
+
do {
|
|
330
|
+
let verificationResult = try await AppTransaction.shared
|
|
331
|
+
switch verificationResult {
|
|
332
|
+
case .verified(let appTransaction):
|
|
333
|
+
let originalBuildNumber = appTransaction.originalAppVersion
|
|
334
|
+
let originalInt = Int(originalBuildNumber) ?? 0
|
|
335
|
+
let targetInt = Int(targetBuildNumber) ?? 0
|
|
336
|
+
call.resolve([
|
|
337
|
+
"isOlderVersion": originalInt < targetInt,
|
|
338
|
+
"originalAppVersion": originalBuildNumber
|
|
339
|
+
])
|
|
340
|
+
case .unverified(_, let error):
|
|
341
|
+
call.reject("App transaction verification failed: \(error.localizedDescription)")
|
|
342
|
+
}
|
|
343
|
+
} catch {
|
|
344
|
+
print("isEntitledToOldBusinessModel error: \(error)")
|
|
345
|
+
call.reject("Failed to get app transaction: \(error.localizedDescription)")
|
|
346
|
+
}
|
|
462
347
|
}
|
|
348
|
+
}
|
|
463
349
|
|
|
350
|
+
@available(iOS 16.0, *)
|
|
351
|
+
private extension AppStore.Environment {
|
|
352
|
+
var environmentString: String {
|
|
353
|
+
switch self {
|
|
354
|
+
case .sandbox: return "Sandbox"
|
|
355
|
+
case .production: return "Production"
|
|
356
|
+
case .xcode: return "Xcode"
|
|
357
|
+
default: return "Production"
|
|
358
|
+
}
|
|
359
|
+
}
|
|
464
360
|
}
|
|
@@ -1,87 +1,61 @@
|
|
|
1
|
-
//
|
|
2
|
-
// TransactionHelpers.swift
|
|
3
|
-
// CapgoNativePurchases
|
|
4
|
-
//
|
|
5
|
-
// Created by Martin DONADIEU
|
|
6
|
-
//
|
|
7
|
-
|
|
8
1
|
import Foundation
|
|
9
2
|
import StoreKit
|
|
10
3
|
|
|
11
|
-
@available(iOS 15.0, *)
|
|
12
4
|
internal class TransactionHelpers {
|
|
13
5
|
|
|
14
|
-
static func buildTransactionResponse(
|
|
6
|
+
static func buildTransactionResponse(
|
|
7
|
+
from transaction: Transaction,
|
|
8
|
+
jwsRepresentation: String? = nil,
|
|
9
|
+
alwaysIncludeWillCancel: Bool = false
|
|
10
|
+
) async -> [String: Any] {
|
|
15
11
|
var response: [String: Any] = ["transactionId": String(transaction.id)]
|
|
16
12
|
|
|
17
|
-
// Always include willCancel key with NSNull() default if requested (for transaction listener)
|
|
18
13
|
if alwaysIncludeWillCancel {
|
|
19
14
|
response["willCancel"] = NSNull()
|
|
20
15
|
}
|
|
21
16
|
|
|
22
|
-
|
|
17
|
+
addReceiptAndJws(to: &response, jws: jwsRepresentation)
|
|
18
|
+
addTransactionDetails(to: &response, transaction: transaction)
|
|
19
|
+
|
|
20
|
+
if transaction.productType == .autoRenewable {
|
|
21
|
+
addSubscriptionInfo(to: &response, transaction: transaction)
|
|
22
|
+
await addRenewalInfo(to: &response, transaction: transaction)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return response
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private static func addReceiptAndJws(to response: inout [String: Any], jws: String?) {
|
|
23
29
|
if let receiptBase64 = getReceiptData() {
|
|
24
30
|
response["receipt"] = receiptBase64
|
|
25
31
|
}
|
|
26
|
-
|
|
27
|
-
// Add StoreKit 2 JWS representation (always available when passed from VerificationResult)
|
|
28
|
-
if let jws = jwsRepresentation {
|
|
32
|
+
if let jws = jws {
|
|
29
33
|
response["jwsRepresentation"] = jws
|
|
30
34
|
}
|
|
35
|
+
}
|
|
31
36
|
|
|
32
|
-
|
|
37
|
+
private static func addTransactionDetails(to response: inout [String: Any], transaction: Transaction) {
|
|
33
38
|
response["productIdentifier"] = transaction.productID
|
|
34
39
|
response["purchaseDate"] = ISO8601DateFormatter().string(from: transaction.purchaseDate)
|
|
35
40
|
response["productType"] = transaction.productType == .autoRenewable ? "subs" : "inapp"
|
|
36
41
|
response["isUpgraded"] = transaction.isUpgraded
|
|
42
|
+
response["ownershipType"] = transaction.ownershipType.descriptionString
|
|
37
43
|
|
|
38
44
|
if let revocationDate = transaction.revocationDate {
|
|
39
45
|
response["revocationDate"] = ISO8601DateFormatter().string(from: revocationDate)
|
|
40
46
|
}
|
|
41
|
-
|
|
42
47
|
if let revocationReason = transaction.revocationReason {
|
|
43
48
|
response["revocationReason"] = revocationReason.descriptionString
|
|
44
49
|
}
|
|
45
|
-
|
|
46
50
|
if #available(iOS 17.0, *) {
|
|
47
51
|
response["transactionReason"] = transaction.reason.descriptionString
|
|
48
52
|
}
|
|
49
|
-
|
|
50
|
-
// Add ownership type (purchased or familyShared)
|
|
51
|
-
switch transaction.ownershipType {
|
|
52
|
-
case .purchased:
|
|
53
|
-
response["ownershipType"] = "purchased"
|
|
54
|
-
case .familyShared:
|
|
55
|
-
response["ownershipType"] = "familyShared"
|
|
56
|
-
default:
|
|
57
|
-
response["ownershipType"] = "purchased"
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Add environment (Sandbox, Production, or Xcode) - iOS 16.0+
|
|
61
53
|
if #available(iOS 16.0, *) {
|
|
62
|
-
|
|
63
|
-
case .sandbox:
|
|
64
|
-
response["environment"] = "Sandbox"
|
|
65
|
-
case .production:
|
|
66
|
-
response["environment"] = "Production"
|
|
67
|
-
case .xcode:
|
|
68
|
-
response["environment"] = "Xcode"
|
|
69
|
-
default:
|
|
70
|
-
response["environment"] = "Production"
|
|
71
|
-
}
|
|
54
|
+
response["environment"] = transaction.environment.descriptionString
|
|
72
55
|
}
|
|
73
|
-
|
|
74
56
|
if let token = transaction.appAccountToken {
|
|
75
57
|
response["appAccountToken"] = token.uuidString
|
|
76
58
|
}
|
|
77
|
-
|
|
78
|
-
// Add subscription-specific information
|
|
79
|
-
if transaction.productType == .autoRenewable {
|
|
80
|
-
addSubscriptionInfo(to: &response, transaction: transaction)
|
|
81
|
-
await addRenewalInfo(to: &response, transaction: transaction)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return response
|
|
85
59
|
}
|
|
86
60
|
|
|
87
61
|
static func getReceiptData() -> String? {
|
|
@@ -94,7 +68,9 @@ internal class TransactionHelpers {
|
|
|
94
68
|
}
|
|
95
69
|
|
|
96
70
|
static func addSubscriptionInfo(to response: inout [String: Any], transaction: Transaction) {
|
|
97
|
-
response["originalPurchaseDate"] = ISO8601DateFormatter().string(
|
|
71
|
+
response["originalPurchaseDate"] = ISO8601DateFormatter().string(
|
|
72
|
+
from: transaction.originalPurchaseDate
|
|
73
|
+
)
|
|
98
74
|
if let expirationDate = transaction.expirationDate {
|
|
99
75
|
response["expirationDate"] = ISO8601DateFormatter().string(from: expirationDate)
|
|
100
76
|
response["isActive"] = expirationDate > Date()
|
|
@@ -102,8 +78,7 @@ internal class TransactionHelpers {
|
|
|
102
78
|
}
|
|
103
79
|
|
|
104
80
|
static func addRenewalInfo(to response: inout [String: Any], transaction: Transaction) async {
|
|
105
|
-
let subscriptionStatus = await transaction.subscriptionStatus
|
|
106
|
-
guard let subscriptionStatus = subscriptionStatus else {
|
|
81
|
+
guard let subscriptionStatus = await transaction.subscriptionStatus else {
|
|
107
82
|
response["willCancel"] = NSNull()
|
|
108
83
|
return
|
|
109
84
|
}
|
|
@@ -111,11 +86,9 @@ internal class TransactionHelpers {
|
|
|
111
86
|
response["subscriptionState"] = subscriptionStatus.state.descriptionString
|
|
112
87
|
|
|
113
88
|
if subscriptionStatus.state == .subscribed {
|
|
114
|
-
let
|
|
115
|
-
switch renewalInfo {
|
|
116
|
-
case .verified(let value):
|
|
89
|
+
if case .verified(let value) = subscriptionStatus.renewalInfo {
|
|
117
90
|
response["willCancel"] = !value.willAutoRenew
|
|
118
|
-
|
|
91
|
+
} else {
|
|
119
92
|
response["willCancel"] = NSNull()
|
|
120
93
|
}
|
|
121
94
|
} else {
|
|
@@ -123,78 +96,49 @@ internal class TransactionHelpers {
|
|
|
123
96
|
}
|
|
124
97
|
}
|
|
125
98
|
|
|
126
|
-
static func shouldFilterTransaction(_ transaction: Transaction, filter: String?) -> Bool {
|
|
127
|
-
guard let filter = filter else { return false }
|
|
128
|
-
let transactionAccountToken = transaction.appAccountToken?.uuidString
|
|
129
|
-
return transactionAccountToken != filter
|
|
130
|
-
}
|
|
131
|
-
|
|
132
99
|
static func collectAllPurchases(appAccountTokenFilter: String?) async -> [[String: Any]] {
|
|
133
100
|
var allPurchases: [[String: Any]] = []
|
|
101
|
+
var seenIds = Set<String>()
|
|
134
102
|
|
|
135
|
-
// Get all current entitlements (active subscriptions)
|
|
136
|
-
await collectCurrentEntitlements(appAccountTokenFilter: appAccountTokenFilter, into: &allPurchases)
|
|
137
|
-
|
|
138
|
-
// Also get all transactions (including non-consumables and expired subscriptions)
|
|
139
|
-
await collectAllTransactions(appAccountTokenFilter: appAccountTokenFilter, into: &allPurchases)
|
|
140
|
-
|
|
141
|
-
return allPurchases
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
static func collectCurrentEntitlements(appAccountTokenFilter: String?, into allPurchases: inout [[String: Any]]) async {
|
|
145
103
|
for await result in Transaction.currentEntitlements {
|
|
146
|
-
guard case .verified(let transaction) = result else {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
continue
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if shouldFilterTransaction(transaction, filter: appAccountTokenFilter) {
|
|
154
|
-
continue
|
|
155
|
-
}
|
|
104
|
+
guard case .verified(let transaction) = result else { continue }
|
|
105
|
+
if let filter = appAccountTokenFilter,
|
|
106
|
+
transaction.appAccountToken?.uuidString != filter { continue }
|
|
156
107
|
|
|
157
|
-
let
|
|
158
|
-
|
|
108
|
+
let idStr = String(transaction.id)
|
|
109
|
+
seenIds.insert(idStr)
|
|
110
|
+
let data = await buildTransactionResponse(
|
|
111
|
+
from: transaction,
|
|
112
|
+
jwsRepresentation: result.jwsRepresentation
|
|
113
|
+
)
|
|
114
|
+
allPurchases.append(data)
|
|
159
115
|
}
|
|
160
|
-
}
|
|
161
116
|
|
|
162
|
-
static func collectAllTransactions(appAccountTokenFilter: String?, into allPurchases: inout [[String: Any]]) async {
|
|
163
117
|
for await result in Transaction.all {
|
|
164
|
-
guard case .verified(let transaction) = result else {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
continue
|
|
169
|
-
}
|
|
118
|
+
guard case .verified(let transaction) = result else { continue }
|
|
119
|
+
if let filter = appAccountTokenFilter,
|
|
120
|
+
transaction.appAccountToken?.uuidString != filter { continue }
|
|
170
121
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
122
|
+
let idStr = String(transaction.id)
|
|
123
|
+
if seenIds.contains(idStr) { continue }
|
|
174
124
|
|
|
175
|
-
let
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if !alreadyExists {
|
|
181
|
-
let purchaseData = await buildTransactionResponse(from: transaction, jwsRepresentation: result.jwsRepresentation)
|
|
182
|
-
allPurchases.append(purchaseData)
|
|
183
|
-
}
|
|
125
|
+
let data = await buildTransactionResponse(
|
|
126
|
+
from: transaction,
|
|
127
|
+
jwsRepresentation: result.jwsRepresentation
|
|
128
|
+
)
|
|
129
|
+
allPurchases.append(data)
|
|
184
130
|
}
|
|
131
|
+
|
|
132
|
+
return allPurchases
|
|
185
133
|
}
|
|
186
134
|
}
|
|
187
135
|
|
|
188
|
-
@available(iOS 15.0, *)
|
|
189
136
|
private extension Transaction.RevocationReason {
|
|
190
137
|
var descriptionString: String {
|
|
191
138
|
switch self {
|
|
192
|
-
case .developerIssue:
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
return "other"
|
|
196
|
-
default:
|
|
197
|
-
return "unknown"
|
|
139
|
+
case .developerIssue: return "developerIssue"
|
|
140
|
+
case .other: return "other"
|
|
141
|
+
default: return "unknown"
|
|
198
142
|
}
|
|
199
143
|
}
|
|
200
144
|
}
|
|
@@ -203,32 +147,44 @@ private extension Transaction.RevocationReason {
|
|
|
203
147
|
private extension Transaction.Reason {
|
|
204
148
|
var descriptionString: String {
|
|
205
149
|
switch self {
|
|
206
|
-
case .purchase:
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
150
|
+
case .purchase: return "purchase"
|
|
151
|
+
case .renewal: return "renewal"
|
|
152
|
+
default: return "unknown"
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private extension Transaction.OwnershipType {
|
|
158
|
+
var descriptionString: String {
|
|
159
|
+
switch self {
|
|
160
|
+
case .purchased: return "purchased"
|
|
161
|
+
case .familyShared: return "familyShared"
|
|
162
|
+
default: return "purchased"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@available(iOS 16.0, *)
|
|
168
|
+
private extension AppStore.Environment {
|
|
169
|
+
var descriptionString: String {
|
|
170
|
+
switch self {
|
|
171
|
+
case .sandbox: return "Sandbox"
|
|
172
|
+
case .production: return "Production"
|
|
173
|
+
case .xcode: return "Xcode"
|
|
174
|
+
default: return "Production"
|
|
212
175
|
}
|
|
213
176
|
}
|
|
214
177
|
}
|
|
215
178
|
|
|
216
|
-
@available(iOS 15.0, *)
|
|
217
179
|
private extension Product.SubscriptionInfo.RenewalState {
|
|
218
180
|
var descriptionString: String {
|
|
219
181
|
switch self {
|
|
220
|
-
case .subscribed:
|
|
221
|
-
|
|
222
|
-
case .
|
|
223
|
-
|
|
224
|
-
case .
|
|
225
|
-
|
|
226
|
-
case .inGracePeriod:
|
|
227
|
-
return "inGracePeriod"
|
|
228
|
-
case .inBillingRetryPeriod:
|
|
229
|
-
return "inBillingRetryPeriod"
|
|
230
|
-
default:
|
|
231
|
-
return "unknown"
|
|
182
|
+
case .subscribed: return "subscribed"
|
|
183
|
+
case .expired: return "expired"
|
|
184
|
+
case .revoked: return "revoked"
|
|
185
|
+
case .inGracePeriod: return "inGracePeriod"
|
|
186
|
+
case .inBillingRetryPeriod: return "inBillingRetryPeriod"
|
|
187
|
+
default: return "unknown"
|
|
232
188
|
}
|
|
233
189
|
}
|
|
234
190
|
}
|