Skip to content

Commit ba88b64

Browse files
committed
Add Transaction.unfinished API and expose appAccountToken in SK2PurchaseDetails
This PR adds two new features to in_app_purchase_storekit for StoreKit 2: 1. SK2Transaction.unfinishedTransactions() - Queries only unfinished transactions, mirroring Apple's Transaction.unfinished API for better performance. 2. SK2PurchaseDetails.appAccountToken - Exposes the UUID that associates transactions with users in custom backend systems. Both features are additive and maintain full backward compatibility. Tests included for both features.
1 parent 4385dea commit ba88b64

File tree

10 files changed

+419
-269
lines changed

10 files changed

+419
-269
lines changed

packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.4.7
2+
3+
* Adds `SK2Transaction.unfinishedTransactions()` method to query only unfinished transactions.
4+
* Exposes `appAccountToken` property in `SK2PurchaseDetails` for user identification.
5+
16
## 0.4.6+1
27

38
* Refactors internals for improved testability.

packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/InAppPurchasePlugin+StoreKit2.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,22 @@ extension InAppPurchasePlugin: InAppPurchase2API {
230230
}
231231
}
232232

233+
/// Wrapper method around StoreKit2's Transaction.unfinished
234+
/// https://developer.apple.com/documentation/storekit/transaction/unfinished
235+
func unfinishedTransactions(
236+
completion: @escaping (Result<[SK2TransactionMessage], Error>) -> Void
237+
) {
238+
Task {
239+
@MainActor in
240+
do {
241+
let transactionsMsgs = await rawUnfinishedTransactions().map {
242+
$0.convertToPigeon(receipt: nil)
243+
}
244+
completion(.success(transactionsMsgs))
245+
}
246+
}
247+
}
248+
233249
func restorePurchases(completion: @escaping (Result<Void, Error>) -> Void) {
234250
Task { [weak self] in
235251
guard let self = self else { return }
@@ -362,6 +378,20 @@ extension InAppPurchasePlugin: InAppPurchase2API {
362378
return transactions
363379
}
364380

381+
/// Helper function that fetches and unwraps all verified unfinished transactions
382+
func rawUnfinishedTransactions() async -> [Transaction] {
383+
var transactions: [Transaction] = []
384+
for await verificationResult in Transaction.unfinished {
385+
switch verificationResult {
386+
case .verified(let transaction):
387+
transactions.append(transaction)
388+
case .unverified:
389+
break
390+
}
391+
}
392+
return transactions
393+
}
394+
365395
/// Helper function to fetch specific transaction
366396
func fetchTransaction(by id: UInt64) async throws -> Transaction? {
367397
for await result in Transaction.all {

packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/sk2_pigeon.g.swift

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ private func nilOrValue<T>(_ value: Any?) -> T? {
7373
return value as! T?
7474
}
7575

76-
// swift-format-ignore: AlwaysUseLowerCamelCase
7776
func deepEqualssk2_pigeon(_ lhs: Any?, _ rhs: Any?) -> Bool {
7877
let cleanLhs = nilOrValue(lhs) as Any?
7978
let cleanRhs = nilOrValue(rhs) as Any?
@@ -115,7 +114,6 @@ func deepEqualssk2_pigeon(_ lhs: Any?, _ rhs: Any?) -> Bool {
115114
}
116115
}
117116

118-
// swift-format-ignore: AlwaysUseLowerCamelCase
119117
func deepHashsk2_pigeon(value: Any?, hasher: inout Hasher) {
120118
if let valueList = value as? [AnyHashable] {
121119
for item in valueList { deepHashsk2_pigeon(value: item, hasher: &hasher) }
@@ -211,6 +209,9 @@ struct SK2SubscriptionOfferMessage: Hashable {
211209
paymentMode,
212210
]
213211
}
212+
static func == (lhs: SK2SubscriptionOfferMessage, rhs: SK2SubscriptionOfferMessage) -> Bool {
213+
return deepEqualssk2_pigeon(lhs.toList(), rhs.toList())
214+
}
214215
func hash(into hasher: inout Hasher) {
215216
deepHashsk2_pigeon(value: toList(), hasher: &hasher)
216217
}
@@ -239,6 +240,9 @@ struct SK2SubscriptionPeriodMessage: Hashable {
239240
unit,
240241
]
241242
}
243+
static func == (lhs: SK2SubscriptionPeriodMessage, rhs: SK2SubscriptionPeriodMessage) -> Bool {
244+
return deepEqualssk2_pigeon(lhs.toList(), rhs.toList())
245+
}
242246
func hash(into hasher: inout Hasher) {
243247
deepHashsk2_pigeon(value: toList(), hasher: &hasher)
244248
}
@@ -272,6 +276,9 @@ struct SK2SubscriptionInfoMessage: Hashable {
272276
subscriptionPeriod,
273277
]
274278
}
279+
static func == (lhs: SK2SubscriptionInfoMessage, rhs: SK2SubscriptionInfoMessage) -> Bool {
280+
return deepEqualssk2_pigeon(lhs.toList(), rhs.toList())
281+
}
275282
func hash(into hasher: inout Hasher) {
276283
deepHashsk2_pigeon(value: toList(), hasher: &hasher)
277284
}
@@ -333,6 +340,9 @@ struct SK2ProductMessage: Hashable {
333340
priceLocale,
334341
]
335342
}
343+
static func == (lhs: SK2ProductMessage, rhs: SK2ProductMessage) -> Bool {
344+
return deepEqualssk2_pigeon(lhs.toList(), rhs.toList())
345+
}
336346
func hash(into hasher: inout Hasher) {
337347
deepHashsk2_pigeon(value: toList(), hasher: &hasher)
338348
}
@@ -359,6 +369,9 @@ struct SK2PriceLocaleMessage: Hashable {
359369
currencySymbol,
360370
]
361371
}
372+
static func == (lhs: SK2PriceLocaleMessage, rhs: SK2PriceLocaleMessage) -> Bool {
373+
return deepEqualssk2_pigeon(lhs.toList(), rhs.toList())
374+
}
362375
func hash(into hasher: inout Hasher) {
363376
deepHashsk2_pigeon(value: toList(), hasher: &hasher)
364377
}
@@ -396,6 +409,11 @@ struct SK2SubscriptionOfferSignatureMessage: Hashable {
396409
signature,
397410
]
398411
}
412+
static func == (
413+
lhs: SK2SubscriptionOfferSignatureMessage, rhs: SK2SubscriptionOfferSignatureMessage
414+
) -> Bool {
415+
return deepEqualssk2_pigeon(lhs.toList(), rhs.toList())
416+
}
399417
func hash(into hasher: inout Hasher) {
400418
deepHashsk2_pigeon(value: toList(), hasher: &hasher)
401419
}
@@ -422,6 +440,11 @@ struct SK2SubscriptionOfferPurchaseMessage: Hashable {
422440
promotionalOfferSignature,
423441
]
424442
}
443+
static func == (
444+
lhs: SK2SubscriptionOfferPurchaseMessage, rhs: SK2SubscriptionOfferPurchaseMessage
445+
) -> Bool {
446+
return deepEqualssk2_pigeon(lhs.toList(), rhs.toList())
447+
}
425448
func hash(into hasher: inout Hasher) {
426449
deepHashsk2_pigeon(value: toList(), hasher: &hasher)
427450
}
@@ -456,6 +479,11 @@ struct SK2ProductPurchaseOptionsMessage: Hashable {
456479
winBackOfferId,
457480
]
458481
}
482+
static func == (lhs: SK2ProductPurchaseOptionsMessage, rhs: SK2ProductPurchaseOptionsMessage)
483+
-> Bool
484+
{
485+
return deepEqualssk2_pigeon(lhs.toList(), rhs.toList())
486+
}
459487
func hash(into hasher: inout Hasher) {
460488
deepHashsk2_pigeon(value: toList(), hasher: &hasher)
461489
}
@@ -518,6 +546,9 @@ struct SK2TransactionMessage: Hashable {
518546
jsonRepresentation,
519547
]
520548
}
549+
static func == (lhs: SK2TransactionMessage, rhs: SK2TransactionMessage) -> Bool {
550+
return deepEqualssk2_pigeon(lhs.toList(), rhs.toList())
551+
}
521552
func hash(into hasher: inout Hasher) {
522553
deepHashsk2_pigeon(value: toList(), hasher: &hasher)
523554
}
@@ -695,6 +726,8 @@ protocol InAppPurchase2API {
695726
func isIntroductoryOfferEligible(
696727
productId: String, completion: @escaping (Result<Bool, Error>) -> Void)
697728
func transactions(completion: @escaping (Result<[SK2TransactionMessage], Error>) -> Void)
729+
func unfinishedTransactions(
730+
completion: @escaping (Result<[SK2TransactionMessage], Error>) -> Void)
698731
func finish(id: Int64, completion: @escaping (Result<Void, Error>) -> Void)
699732
func startListeningToTransactions() throws
700733
func stopListeningToTransactions() throws
@@ -828,6 +861,24 @@ class InAppPurchase2APISetup {
828861
} else {
829862
transactionsChannel.setMessageHandler(nil)
830863
}
864+
let unfinishedTransactionsChannel = FlutterBasicMessageChannel(
865+
name:
866+
"dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchase2API.unfinishedTransactions\(channelSuffix)",
867+
binaryMessenger: binaryMessenger, codec: codec)
868+
if let api = api {
869+
unfinishedTransactionsChannel.setMessageHandler { _, reply in
870+
api.unfinishedTransactions { result in
871+
switch result {
872+
case .success(let res):
873+
reply(wrapResult(res))
874+
case .failure(let error):
875+
reply(wrapError(error))
876+
}
877+
}
878+
}
879+
} else {
880+
unfinishedTransactionsChannel.setMessageHandler(nil)
881+
}
831882
let finishChannel = FlutterBasicMessageChannel(
832883
name: "dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchase2API.finish\(channelSuffix)",
833884
binaryMessenger: binaryMessenger, codec: codec)

0 commit comments

Comments
 (0)