blob: d18a09cfa405d86f6feac509d6670fab6ca5dd11 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "FIAPaymentQueueHandler.h"
#import "FIAPPaymentQueueDelegate.h"
#import "FIATransactionCache.h"
@interface FIAPaymentQueueHandler ()
/// The SKPaymentQueue instance connected to the App Store and responsible for processing
/// transactions.
@property(strong, nonatomic) SKPaymentQueue *queue;
/// Callback method that is called each time the App Store indicates transactions are updated.
@property(nullable, copy, nonatomic) TransactionsUpdated transactionsUpdated;
/// Callback method that is called each time the App Store indicates transactions are removed.
@property(nullable, copy, nonatomic) TransactionsRemoved transactionsRemoved;
/// Callback method that is called each time the App Store indicates transactions failed to restore.
@property(nullable, copy, nonatomic) RestoreTransactionFailed restoreTransactionFailed;
/// Callback method that is called each time the App Store indicates restoring of transactions has
/// finished.
@property(nullable, copy, nonatomic)
RestoreCompletedTransactionsFinished paymentQueueRestoreCompletedTransactionsFinished;
/// Callback method that is called each time an in-app purchase has been initiated from the App
/// Store.
@property(nullable, copy, nonatomic) ShouldAddStorePayment shouldAddStorePayment;
/// Callback method that is called each time the App Store indicates downloads are updated.
@property(nullable, copy, nonatomic) UpdatedDownloads updatedDownloads;
/// The transaction cache responsible for caching transactions.
///
/// Keeps track of transactions that arrive when the Flutter client is not
/// actively observing for transactions.
@property(strong, nonatomic, nonnull) FIATransactionCache *transactionCache;
/// Indicates if the Flutter client is observing transactions.
///
/// When the client is not observing, transactions are cached and send to the
/// client as soon as it starts observing. The Flutter client can start
/// observing by sending a startObservingPaymentQueue message and stop by
/// sending a stopObservingPaymentQueue message.
@property(atomic, assign, readwrite, getter=isObservingTransactions) BOOL observingTransactions;
@end
@implementation FIAPaymentQueueHandler
- (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue
transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated
transactionRemoved:(nullable TransactionsRemoved)transactionsRemoved
restoreTransactionFailed:(nullable RestoreTransactionFailed)restoreTransactionFailed
restoreCompletedTransactionsFinished:
(nullable RestoreCompletedTransactionsFinished)restoreCompletedTransactionsFinished
shouldAddStorePayment:(nullable ShouldAddStorePayment)shouldAddStorePayment
updatedDownloads:(nullable UpdatedDownloads)updatedDownloads {
return [[FIAPaymentQueueHandler alloc] initWithQueue:queue
transactionsUpdated:transactionsUpdated
transactionRemoved:transactionsRemoved
restoreTransactionFailed:restoreTransactionFailed
restoreCompletedTransactionsFinished:restoreCompletedTransactionsFinished
shouldAddStorePayment:shouldAddStorePayment
updatedDownloads:updatedDownloads
transactionCache:[[FIATransactionCache alloc] init]];
}
- (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue
transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated
transactionRemoved:(nullable TransactionsRemoved)transactionsRemoved
restoreTransactionFailed:(nullable RestoreTransactionFailed)restoreTransactionFailed
restoreCompletedTransactionsFinished:
(nullable RestoreCompletedTransactionsFinished)restoreCompletedTransactionsFinished
shouldAddStorePayment:(nullable ShouldAddStorePayment)shouldAddStorePayment
updatedDownloads:(nullable UpdatedDownloads)updatedDownloads
transactionCache:(nonnull FIATransactionCache *)transactionCache {
self = [super init];
if (self) {
_queue = queue;
_transactionsUpdated = transactionsUpdated;
_transactionsRemoved = transactionsRemoved;
_restoreTransactionFailed = restoreTransactionFailed;
_paymentQueueRestoreCompletedTransactionsFinished = restoreCompletedTransactionsFinished;
_shouldAddStorePayment = shouldAddStorePayment;
_updatedDownloads = updatedDownloads;
_transactionCache = transactionCache;
[_queue addTransactionObserver:self];
if (@available(iOS 13.0, macOS 10.15, *)) {
queue.delegate = self.delegate;
}
}
return self;
}
- (void)startObservingPaymentQueue {
self.observingTransactions = YES;
[self processCachedTransactions];
}
- (void)stopObservingPaymentQueue {
// When the client stops observing transaction, the transaction observer is
// not removed from the SKPaymentQueue. The FIAPaymentQueueHandler will cache
// trasnactions in memory when the client is not observing, allowing the app
// to process these transactions if it starts observing again during the same
// lifetime of the app.
//
// If the app is killed, cached transactions will be removed from memory;
// however, the App Store will re-deliver the transactions as soon as the app
// is started again, since the cached transactions have not been acknowledged
// by the client (by sending the `finishTransaction` message).
self.observingTransactions = NO;
}
- (void)processCachedTransactions {
NSArray *cachedObjects =
[self.transactionCache getObjectsForKey:TransactionCacheKeyUpdatedTransactions];
if (cachedObjects.count != 0) {
self.transactionsUpdated(cachedObjects);
}
cachedObjects = [self.transactionCache getObjectsForKey:TransactionCacheKeyUpdatedDownloads];
if (cachedObjects.count != 0) {
self.updatedDownloads(cachedObjects);
}
cachedObjects = [self.transactionCache getObjectsForKey:TransactionCacheKeyRemovedTransactions];
if (cachedObjects.count != 0) {
self.transactionsRemoved(cachedObjects);
}
[self.transactionCache clear];
}
- (BOOL)addPayment:(SKPayment *)payment {
for (SKPaymentTransaction *transaction in self.queue.transactions) {
if ([transaction.payment.productIdentifier isEqualToString:payment.productIdentifier]) {
return NO;
}
}
[self.queue addPayment:payment];
return YES;
}
- (void)finishTransaction:(SKPaymentTransaction *)transaction {
[self.queue finishTransaction:transaction];
}
- (void)restoreTransactions:(nullable NSString *)applicationName {
if (applicationName) {
[self.queue restoreCompletedTransactionsWithApplicationUsername:applicationName];
} else {
[self.queue restoreCompletedTransactions];
}
}
#if TARGET_OS_IOS
- (void)presentCodeRedemptionSheet {
if (@available(iOS 14, *)) {
[self.queue presentCodeRedemptionSheet];
} else {
NSLog(@"presentCodeRedemptionSheet is only available on iOS 14 or newer");
}
}
#endif
#if TARGET_OS_IOS
- (void)showPriceConsentIfNeeded {
[self.queue showPriceConsentIfNeeded];
}
#endif
#pragma mark - observing
// Sent when the transaction array has changed (additions or state changes). Client should check
// state of transactions and finish as appropriate.
- (void)paymentQueue:(SKPaymentQueue *)queue
updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
if (!self.observingTransactions) {
[_transactionCache addObjects:transactions forKey:TransactionCacheKeyUpdatedTransactions];
return;
}
// notify dart through callbacks.
self.transactionsUpdated(transactions);
}
// Sent when transactions are removed from the queue (via finishTransaction:).
- (void)paymentQueue:(SKPaymentQueue *)queue
removedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
if (!self.observingTransactions) {
[_transactionCache addObjects:transactions forKey:TransactionCacheKeyRemovedTransactions];
return;
}
self.transactionsRemoved(transactions);
}
// Sent when an error is encountered while adding transactions from the user's purchase history back
// to the queue.
- (void)paymentQueue:(SKPaymentQueue *)queue
restoreCompletedTransactionsFailedWithError:(NSError *)error {
self.restoreTransactionFailed(error);
}
// Sent when all transactions from the user's purchase history have successfully been added back to
// the queue.
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue {
self.paymentQueueRestoreCompletedTransactionsFinished();
}
// Sent when the download state has changed.
- (void)paymentQueue:(SKPaymentQueue *)queue updatedDownloads:(NSArray<SKDownload *> *)downloads {
if (!self.observingTransactions) {
[_transactionCache addObjects:downloads forKey:TransactionCacheKeyUpdatedDownloads];
return;
}
self.updatedDownloads(downloads);
}
// Sent when a user initiates an IAP buy from the App Store
- (BOOL)paymentQueue:(SKPaymentQueue *)queue
shouldAddStorePayment:(SKPayment *)payment
forProduct:(SKProduct *)product {
return (self.shouldAddStorePayment(payment, product));
}
- (NSArray<SKPaymentTransaction *> *)getUnfinishedTransactions {
return self.queue.transactions;
}
@end