Skip to content

Commit 94485f1

Browse files
authored
[in_app_purchase_storekit] Group purchases into a single event in storekit2 (#12237)
*Replace this paragraph with a description of what this PR is changing or adding, and why. Consider including before/after screenshots.* *List which issues are fixed by this PR. You must list at least one issue.* Fixes flutter/flutter#187355 ## Pre-Review Checklist **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
1 parent 4997dab commit 94485f1

4 files changed

Lines changed: 64 additions & 7 deletions

File tree

packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.4.11+1
2+
3+
* Fixes StoreKit 2 restore transactions not grouping purchases into a single event.
4+
15
## 0.4.11
26

37
* Fixes StoreKit 2 date format does not match in_app_purchase_platform_interface PurchaseDetails.transactionDate format.

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -279,22 +279,24 @@ extension InAppPurchasePlugin: InAppPurchase2API {
279279
Task { [weak self] in
280280
guard let self = self else { return }
281281
do {
282+
var restoredTransactions: [SK2TransactionMessage] = []
282283
var unverifiedPurchases: [UInt64: (receipt: String, error: Error?)] = [:]
283284
for await completedPurchase in Transaction.currentEntitlements {
284285
switch completedPurchase {
285286
case .verified(let purchase):
286-
self.sendTransactionUpdate(
287-
productId: purchase.productID,
288-
transaction: purchase,
289-
receipt: "\(completedPurchase.jwsRepresentation)",
290-
status: .restored
287+
restoredTransactions.append(
288+
purchase.convertToPigeon(
289+
receipt: "\(completedPurchase.jwsRepresentation)",
290+
status: .restored
291+
)
291292
)
292293
case .unverified(let failedPurchase, let error):
293294
unverifiedPurchases[failedPurchase.id] = (
294295
receipt: completedPurchase.jwsRepresentation, error: error
295296
)
296297
}
297298
}
299+
self.sendTransactionUpdates(restoredTransactions)
298300
if !unverifiedPurchases.isEmpty {
299301
completion(
300302
.failure(
@@ -303,6 +305,7 @@ extension InAppPurchasePlugin: InAppPurchase2API {
303305
message:
304306
"This purchase could not be restored.",
305307
details: unverifiedPurchases)))
308+
return
306309
}
307310
completion(.success(Void()))
308311
}
@@ -473,8 +476,12 @@ extension InAppPurchasePlugin: InAppPurchase2API {
473476
)
474477
}
475478

479+
sendTransactionUpdates([transactionMessage])
480+
}
481+
482+
private func sendTransactionUpdates(_ transactionMessages: [SK2TransactionMessage]) {
476483
Task { @MainActor in
477-
self.transactionCallbackAPI?.onTransactionsUpdated(newTransactions: [transactionMessage]) {
484+
self.transactionCallbackAPI?.onTransactionsUpdated(newTransactions: transactionMessages) {
478485
result in
479486
switch result {
480487
case .success: break

packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchaseStoreKit2PluginTests.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,52 @@ final class InAppPurchase2PluginTests: XCTestCase {
420420
XCTAssert(callback.lastUpdate.first?.status == .restored)
421421
}
422422

423+
func testRestoreMultipleProductsEmitsSingleBatchedUpdate() async throws {
424+
// Purchase two subscriptions from different subscription groups so that
425+
// both persist in `currentEntitlements` and restoring returns two
426+
// transactions.
427+
let firstPurchaseExpectation = self.expectation(description: "First purchase should succeed")
428+
plugin.purchase(id: "subscription_discounted", options: nil) { result in
429+
switch result {
430+
case .success:
431+
firstPurchaseExpectation.fulfill()
432+
case .failure(let error):
433+
XCTFail("Purchase should NOT fail. Failed with \(error)")
434+
}
435+
}
436+
await fulfillment(of: [firstPurchaseExpectation], timeout: 5)
437+
438+
let secondPurchaseExpectation = self.expectation(description: "Second purchase should succeed")
439+
plugin.purchase(id: "subscription_silver", options: nil) { result in
440+
switch result {
441+
case .success:
442+
secondPurchaseExpectation.fulfill()
443+
case .failure(let error):
444+
XCTFail("Purchase should NOT fail. Failed with \(error)")
445+
}
446+
}
447+
await fulfillment(of: [secondPurchaseExpectation], timeout: 5)
448+
449+
let restoreExpectation = self.expectation(description: "Restore request should succeed")
450+
plugin.restorePurchases { result in
451+
switch result {
452+
case .success():
453+
restoreExpectation.fulfill()
454+
case .failure(let error):
455+
XCTFail("Restore purchases should NOT fail. Failed with \(error)")
456+
}
457+
}
458+
await fulfillment(of: [restoreExpectation], timeout: 5)
459+
460+
// Both restored transactions must arrive in a single `onTransactionsUpdated`
461+
// callback.
462+
XCTAssertEqual(callback.lastUpdate.count, 2)
463+
XCTAssertTrue(callback.lastUpdate.allSatisfy { $0.status == .restored })
464+
XCTAssertEqual(
465+
Set(callback.lastUpdate.map { $0.productId }),
466+
["subscription_discounted", "subscription_silver"])
467+
}
468+
423469
func testFinishTransaction() async throws {
424470
let purchaseExpectation = self.expectation(description: "Purchase should succeed")
425471
let finishExpectation = self.expectation(description: "Finishing purchase should succeed")

packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: in_app_purchase_storekit
22
description: An implementation for the iOS and macOS platforms of the Flutter `in_app_purchase` plugin. This uses the StoreKit Framework.
33
repository: https://github.com/flutter/packages/tree/main/packages/in_app_purchase/in_app_purchase_storekit
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22
5-
version: 0.4.11
5+
version: 0.4.11+1
66

77
environment:
88
sdk: ^3.10.0

0 commit comments

Comments
 (0)