-
Notifications
You must be signed in to change notification settings - Fork 3.3k
[in_app_purchase_storekit] Add support for purchase and transactions #7574
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b126a15
ae9095a
c38b66f
268a50e
f6ec07a
aa36dfd
8724d40
a6cbd98
b75eec7
4d2fd59
aeef979
acf499f
c3c7204
9021b6f
666df56
97b746f
add5bf5
06b42d0
0701d2a
8f225c0
2c6efdb
72ce51c
2e940fe
442c158
f48510b
6076bf1
d8dcad2
9c2893b
1a74fb7
1b688ae
74c2de3
71c9a1b
582156b
e6bec70
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,16 +4,17 @@ | |
|
||
@available(iOS 15.0, macOS 12.0, *) | ||
extension InAppPurchasePlugin: InAppPurchase2API { | ||
|
||
// MARK: - Pigeon Functions | ||
|
||
// Wrapper method around StoreKit2's canMakePayments() method | ||
// https://developer.apple.com/documentation/storekit/appstore/3822277-canmakepayments | ||
/// Wrapper method around StoreKit2's canMakePayments() method | ||
/// https://developer.apple.com/documentation/storekit/appstore/3822277-canmakepayments | ||
func canMakePayments() throws -> Bool { | ||
return AppStore.canMakePayments | ||
} | ||
|
||
// Wrapper method around StoreKit2's products() method | ||
// https://developer.apple.com/documentation/storekit/product/3851116-products | ||
/// Wrapper method around StoreKit2's products() method | ||
/// https://developer.apple.com/documentation/storekit/product/3851116-products | ||
func products( | ||
identifiers: [String], completion: @escaping (Result<[SK2ProductMessage], Error>) -> Void | ||
) { | ||
|
@@ -34,4 +35,144 @@ extension InAppPurchasePlugin: InAppPurchase2API { | |
} | ||
} | ||
} | ||
|
||
/// Gets the appropriate product, then calls purchase on it. | ||
/// https://developer.apple.com/documentation/storekit/product/3791971-purchase | ||
func purchase( | ||
id: String, options: SK2ProductPurchaseOptionsMessage?, | ||
completion: @escaping (Result<SK2ProductPurchaseResultMessage, Error>) -> Void | ||
) { | ||
Task { @MainActor in | ||
do { | ||
guard let product = try await Product.products(for: [id]).first else { | ||
let error = PigeonError( | ||
code: "storekit2_failed_to_fetch_product", | ||
message: "Storekit has failed to fetch this product.", | ||
details: "Product ID : \(id)") | ||
return completion(.failure(error)) | ||
} | ||
|
||
let result = try await product.purchase(options: []) | ||
|
||
switch result { | ||
case .success(let verification): | ||
switch verification { | ||
case .verified(let transaction): | ||
self.sendTransactionUpdate(transaction: transaction) | ||
completion(.success(result.convertToPigeon())) | ||
case .unverified(_, let error): | ||
completion(.failure(error)) | ||
} | ||
case .pending: | ||
completion( | ||
.failure( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. reduce indentation by combining these There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the formatter uncombines these There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. strange. it's clearly less than 100 characters. how's your code like before the formatter? |
||
PigeonError( | ||
code: "storekit2_purchase_pending", | ||
message: | ||
"This transaction is still pending and but may complete in the future. If it completes, it will be delivered via `purchaseStream`", | ||
details: "Product ID : \(id)"))) | ||
case .userCancelled: | ||
completion( | ||
.failure( | ||
PigeonError( | ||
code: "storekit2_purchase_cancelled", | ||
message: "This transaction has been cancelled by the user.", | ||
details: "Product ID : \(id)"))) | ||
@unknown default: | ||
fatalError("An unknown StoreKit PurchaseResult has been encountered.") | ||
} | ||
} catch { | ||
completion(.failure(error)) | ||
} | ||
} | ||
} | ||
|
||
/// Wrapper method around StoreKit2's transactions() method | ||
/// https://developer.apple.com/documentation/storekit/product/3851116-products | ||
func transactions( | ||
completion: @escaping (Result<[SK2TransactionMessage], Error>) -> Void | ||
) { | ||
Task { | ||
@MainActor in | ||
do { | ||
let transactionsMsgs = await rawTransactions().map { | ||
$0.convertToPigeon() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: use keypath There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Keypath can’t be used on instance methods" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh you are right |
||
} | ||
completion(.success(transactionsMsgs)) | ||
} | ||
} | ||
} | ||
|
||
/// Wrapper method around StoreKit2's finish() method https://developer.apple.com/documentation/storekit/transaction/3749694-finish | ||
func finish(id: Int64, completion: @escaping (Result<Void, Error>) -> Void) { | ||
Task { | ||
let transaction = try await fetchTransaction(by: UInt64(id)) | ||
if let transaction = transaction { | ||
await transaction.finish() | ||
} | ||
} | ||
} | ||
|
||
/// This Task listens to Transation.updates as shown here | ||
/// https://developer.apple.com/documentation/storekit/transaction/3851206-updates | ||
/// This function should be called as soon as the app starts to avoid missing any Transactions done outside of the app. | ||
func startListeningToTransactions() throws { | ||
self.setListenerTaskAsTask( | ||
task: Task { [weak self] in | ||
for await verificationResult in Transaction.updates { | ||
switch verificationResult { | ||
case .verified(let transaction): | ||
self?.sendTransactionUpdate(transaction: transaction) | ||
case .unverified: | ||
break | ||
} | ||
} | ||
}) | ||
} | ||
|
||
/// Stop subscribing to Transaction.updates | ||
func stopListeningToTransactions() throws { | ||
getListenerTaskAsTask.cancel() | ||
} | ||
|
||
/// Sends an transaction back to Dart. Access these transactions with `purchaseStream` | ||
func sendTransactionUpdate(transaction: Transaction) { | ||
let transactionMessage = transaction.convertToPigeon() | ||
transactionCallbackAPI?.onTransactionsUpdated(newTransaction: transactionMessage) { result in | ||
switch result { | ||
case .success: break | ||
case .failure(let error): | ||
print("Failed to send transaction updates: \(error)") | ||
} | ||
} | ||
} | ||
|
||
/// Helper function that fetches and unwraps all verified transactions | ||
private func rawTransactions() async -> [Transaction] { | ||
var transactions: [Transaction] = [] | ||
for await verificationResult in Transaction.all { | ||
switch verificationResult { | ||
case .verified(let transaction): | ||
transactions.append(transaction) | ||
case .unverified: | ||
break | ||
} | ||
} | ||
return transactions | ||
} | ||
|
||
/// Helper function to fetch specific transaction | ||
private func fetchTransaction(by id: UInt64) async throws -> Transaction? { | ||
for await result in Transaction.all { | ||
switch result { | ||
case .verified(let transaction): | ||
if transaction.id == id { | ||
return transaction | ||
} | ||
case .unverified: | ||
continue | ||
} | ||
} | ||
return nil | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can change it in next PR - getter shouldn't have a
get
prefix. It should be justvar updateListenerTask: Task<(), Never>