Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## NEXT
## 0.4.7+1

* Fixes StoreKit 2 purchase flow to send cancelled/pending/unverified results to `purchaseStream`.
* Fixes Xcode 26.2 analyzer warnings in example app tests.

## 0.4.7
Expand All @@ -18,7 +19,7 @@
## 0.4.6

* Adds a new case `.unverified` to enum `SK2ProductPurchaseResult`
* Fixes the StoreKit2 implementation throwing `PlatformException`s instead of returning the corresponding
* Fixes the StoreKit2 implementation throwing `PlatformException`s instead of returning the corresponding
`SK2ProductPurchaseResult` when a purchase is cancelled / unverified / pending.

## 0.4.5
Expand Down Expand Up @@ -47,7 +48,7 @@

* Updates minimum supported SDK version to Flutter 3.27/Dart 3.6.
* Adds **Win Back Offers** support for StoreKit2:
- Includes new `isWinBackOfferEligible` function for eligibility verification
* Includes new `isWinBackOfferEligible` function for eligibility verification
* Adds **Promotional Offers** support in StoreKit2 purchases
* Fixes introductory pricing handling in promotional offers list in StoreKit2
* Ensures proper `appAccountToken` handling for StoreKit2 purchases
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,15 @@ extension InAppPurchasePlugin: InAppPurchase2API {
switch result {
case .success(let verification):
sendTransactionUpdate(
transaction: verification.unsafePayloadValue, receipt: verification.jwsRepresentation)
case .pending, .userCancelled:
break
productId: id,
transaction: verification.unsafePayloadValue,
receipt: verification.jwsRepresentation,
status: .purchased
)
case .pending:
sendTransactionUpdate(productId: id, status: .pending)
case .userCancelled:
sendTransactionUpdate(productId: id, status: .cancelled)
@unknown default:
fatalError("An unknown StoreKit PurchaseResult has been encountered.")
}
Expand Down Expand Up @@ -223,7 +229,7 @@ extension InAppPurchasePlugin: InAppPurchase2API {
@MainActor in
do {
let transactionsMsgs = await rawTransactions().map {
$0.convertToPigeon(receipt: nil)
$0.convertToPigeon(receipt: nil, status: .purchased)
}
completion(.success(transactionsMsgs))
}
Expand All @@ -242,7 +248,8 @@ extension InAppPurchasePlugin: InAppPurchase2API {
switch verificationResult {
case .verified(let transaction):
transactionsMsgs.append(
transaction.convertToPigeon(receipt: verificationResult.jwsRepresentation)
transaction.convertToPigeon(
receipt: verificationResult.jwsRepresentation, status: .purchased)
)
case .unverified:
break
Expand All @@ -261,7 +268,11 @@ extension InAppPurchasePlugin: InAppPurchase2API {
switch completedPurchase {
case .verified(let purchase):
self.sendTransactionUpdate(
transaction: purchase, receipt: "\(completedPurchase.jwsRepresentation)")
productId: purchase.productID,
transaction: purchase,
receipt: "\(completedPurchase.jwsRepresentation)",
status: .restored
)
case .unverified(let failedPurchase, let error):
unverifiedPurchases[failedPurchase.id] = (
receipt: completedPurchase.jwsRepresentation, error: error
Expand Down Expand Up @@ -340,7 +351,11 @@ extension InAppPurchasePlugin: InAppPurchase2API {
switch verificationResult {
case .verified(let transaction):
self?.sendTransactionUpdate(
transaction: transaction, receipt: verificationResult.jwsRepresentation)
productId: transaction.productID,
transaction: transaction,
receipt: verificationResult.jwsRepresentation,
status: .purchased
)
case .unverified:
break
}
Expand All @@ -353,16 +368,41 @@ extension InAppPurchasePlugin: InAppPurchase2API {
updateListenerTask.cancel()
}

/// Sends an transaction back to Dart. Access these transactions with `purchaseStream`
private func sendTransactionUpdate(transaction: Transaction, receipt: String? = nil) {
let transactionMessage = transaction.convertToPigeon(receipt: receipt)
/// Sends a transaction or status update back to Dart. Access these transactions with `purchaseStream`
/// - Parameters:
/// - productId: The product ID (required)
/// - transaction: The transaction object (for success cases, nil for pending/cancelled)
/// - receipt: The JWS receipt data
/// - status: The purchase status
private func sendTransactionUpdate(
productId: String,
transaction: Transaction? = nil,
receipt: String? = nil,
status: SK2PurchaseStatusMessage
) {
let transactionMessage: SK2TransactionMessage

if let transaction = transaction {
// Has real transaction: use transaction info
transactionMessage = transaction.convertToPigeon(receipt: receipt, status: status)
} else {
// No transaction (pending/cancelled): create minimal message without purchaseDate
transactionMessage = SK2TransactionMessage(
id: 0,
originalId: 0,
productId: productId,
purchasedQuantity: 1,
status: status
)
}

Task { @MainActor in
self.transactionCallbackAPI?.onTransactionsUpdated(newTransactions: [transactionMessage]) {
result in
switch result {
case .success: break
case .failure(let error):
print("Failed to send transaction updates: \(error)")
print("Failed to send transaction update: \(error)")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Autogenerated from Pigeon (v26.1.1), do not edit directly.
// Autogenerated from Pigeon (v26.1.5), do not edit directly.
// See also: https://pub.dev/packages/pigeon

import Foundation
Expand Down Expand Up @@ -172,6 +172,19 @@ enum SK2ProductPurchaseResultMessage: Int {
case pending = 3
}

/// The status of a purchase transaction.
/// Used to communicate the result state to Dart layer via purchaseStream.
enum SK2PurchaseStatusMessage: Int {
/// Purchase completed successfully.
case purchased = 0
/// Purchase is pending (e.g., Ask to Buy).
case pending = 1
/// Purchase was cancelled by the user.
case cancelled = 2
/// Purchase was restored.
case restored = 3
}

/// Generated class from Pigeon that represents data sent in messages.
struct SK2SubscriptionOfferMessage: Hashable {
var id: String? = nil
Expand Down Expand Up @@ -494,28 +507,30 @@ struct SK2TransactionMessage: Hashable {
var id: Int64
var originalId: Int64
var productId: String
var purchaseDate: String
var purchaseDate: String? = nil
var expirationDate: String? = nil
var purchasedQuantity: Int64
var appAccountToken: String? = nil
var restoring: Bool
var receiptData: String? = nil
var error: SK2ErrorMessage? = nil
var jsonRepresentation: String? = nil
/// The status of this purchase transaction.
/// Set by native side to communicate the result state to Dart layer.
var status: SK2PurchaseStatusMessage

// swift-format-ignore: AlwaysUseLowerCamelCase
static func fromList(_ pigeonVar_list: [Any?]) -> SK2TransactionMessage? {
let id = pigeonVar_list[0] as! Int64
let originalId = pigeonVar_list[1] as! Int64
let productId = pigeonVar_list[2] as! String
let purchaseDate = pigeonVar_list[3] as! String
let purchaseDate: String? = nilOrValue(pigeonVar_list[3])
let expirationDate: String? = nilOrValue(pigeonVar_list[4])
let purchasedQuantity = pigeonVar_list[5] as! Int64
let appAccountToken: String? = nilOrValue(pigeonVar_list[6])
let restoring = pigeonVar_list[7] as! Bool
let receiptData: String? = nilOrValue(pigeonVar_list[8])
let error: SK2ErrorMessage? = nilOrValue(pigeonVar_list[9])
let jsonRepresentation: String? = nilOrValue(pigeonVar_list[10])
let receiptData: String? = nilOrValue(pigeonVar_list[7])
let error: SK2ErrorMessage? = nilOrValue(pigeonVar_list[8])
let jsonRepresentation: String? = nilOrValue(pigeonVar_list[9])
let status = pigeonVar_list[10] as! SK2PurchaseStatusMessage

return SK2TransactionMessage(
id: id,
Expand All @@ -525,10 +540,10 @@ struct SK2TransactionMessage: Hashable {
expirationDate: expirationDate,
purchasedQuantity: purchasedQuantity,
appAccountToken: appAccountToken,
restoring: restoring,
receiptData: receiptData,
error: error,
jsonRepresentation: jsonRepresentation
jsonRepresentation: jsonRepresentation,
status: status
)
}
func toList() -> [Any?] {
Expand All @@ -540,10 +555,10 @@ struct SK2TransactionMessage: Hashable {
expirationDate,
purchasedQuantity,
appAccountToken,
restoring,
receiptData,
error,
jsonRepresentation,
status,
]
}
static func == (lhs: SK2TransactionMessage, rhs: SK2TransactionMessage) -> Bool {
Expand Down Expand Up @@ -621,24 +636,30 @@ private class StoreKit2MessagesPigeonCodecReader: FlutterStandardReader {
}
return nil
case 134:
return SK2SubscriptionOfferMessage.fromList(self.readValue() as! [Any?])
let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?)
if let enumResultAsInt = enumResultAsInt {
return SK2PurchaseStatusMessage(rawValue: enumResultAsInt)
}
return nil
case 135:
return SK2SubscriptionPeriodMessage.fromList(self.readValue() as! [Any?])
return SK2SubscriptionOfferMessage.fromList(self.readValue() as! [Any?])
case 136:
return SK2SubscriptionInfoMessage.fromList(self.readValue() as! [Any?])
return SK2SubscriptionPeriodMessage.fromList(self.readValue() as! [Any?])
case 137:
return SK2ProductMessage.fromList(self.readValue() as! [Any?])
return SK2SubscriptionInfoMessage.fromList(self.readValue() as! [Any?])
case 138:
return SK2PriceLocaleMessage.fromList(self.readValue() as! [Any?])
return SK2ProductMessage.fromList(self.readValue() as! [Any?])
case 139:
return SK2SubscriptionOfferSignatureMessage.fromList(self.readValue() as! [Any?])
return SK2PriceLocaleMessage.fromList(self.readValue() as! [Any?])
case 140:
return SK2SubscriptionOfferPurchaseMessage.fromList(self.readValue() as! [Any?])
return SK2SubscriptionOfferSignatureMessage.fromList(self.readValue() as! [Any?])
case 141:
return SK2ProductPurchaseOptionsMessage.fromList(self.readValue() as! [Any?])
return SK2SubscriptionOfferPurchaseMessage.fromList(self.readValue() as! [Any?])
case 142:
return SK2TransactionMessage.fromList(self.readValue() as! [Any?])
return SK2ProductPurchaseOptionsMessage.fromList(self.readValue() as! [Any?])
case 143:
return SK2TransactionMessage.fromList(self.readValue() as! [Any?])
case 144:
return SK2ErrorMessage.fromList(self.readValue() as! [Any?])
default:
return super.readValue(ofType: type)
Expand All @@ -663,35 +684,38 @@ private class StoreKit2MessagesPigeonCodecWriter: FlutterStandardWriter {
} else if let value = value as? SK2ProductPurchaseResultMessage {
super.writeByte(133)
super.writeValue(value.rawValue)
} else if let value = value as? SK2SubscriptionOfferMessage {
} else if let value = value as? SK2PurchaseStatusMessage {
super.writeByte(134)
super.writeValue(value.rawValue)
} else if let value = value as? SK2SubscriptionOfferMessage {
super.writeByte(135)
super.writeValue(value.toList())
} else if let value = value as? SK2SubscriptionPeriodMessage {
super.writeByte(135)
super.writeByte(136)
super.writeValue(value.toList())
} else if let value = value as? SK2SubscriptionInfoMessage {
super.writeByte(136)
super.writeByte(137)
super.writeValue(value.toList())
} else if let value = value as? SK2ProductMessage {
super.writeByte(137)
super.writeByte(138)
super.writeValue(value.toList())
} else if let value = value as? SK2PriceLocaleMessage {
super.writeByte(138)
super.writeByte(139)
super.writeValue(value.toList())
} else if let value = value as? SK2SubscriptionOfferSignatureMessage {
super.writeByte(139)
super.writeByte(140)
super.writeValue(value.toList())
} else if let value = value as? SK2SubscriptionOfferPurchaseMessage {
super.writeByte(140)
super.writeByte(141)
super.writeValue(value.toList())
} else if let value = value as? SK2ProductPurchaseOptionsMessage {
super.writeByte(141)
super.writeByte(142)
super.writeValue(value.toList())
} else if let value = value as? SK2TransactionMessage {
super.writeByte(142)
super.writeByte(143)
super.writeValue(value.toList())
} else if let value = value as? SK2ErrorMessage {
super.writeByte(143)
super.writeByte(144)
super.writeValue(value.toList())
} else {
super.writeValue(value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,8 @@ extension Product.PurchaseResult {

@available(iOS 15.0, macOS 12.0, *)
extension Transaction {
func convertToPigeon(receipt: String?) -> SK2TransactionMessage {
func convertToPigeon(receipt: String?, status: SK2PurchaseStatusMessage) -> SK2TransactionMessage
{

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
Expand All @@ -205,9 +206,9 @@ extension Transaction {
expirationDate: expirationDate.map { dateFormatter.string(from: $0) },
purchasedQuantity: Int64(purchasedQuantity),
appAccountToken: appAccountToken?.uuidString,
restoring: receipt != nil,
receiptData: receipt,
jsonRepresentation: String(decoding: jsonRepresentation, as: UTF8.self)
jsonRepresentation: String(decoding: jsonRepresentation, as: UTF8.self),
status: status
)
}
}
Loading