From 3d21006aeaf93429b7bdcd08def35ebaeac9d974 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 15 Feb 2019 10:20:50 -0800 Subject: [PATCH 01/64] draft --- .../ios/Classes/InAppPurchasePlugin.h | 4 ++ .../ios/Classes/InAppPurchasePlugin.m | 50 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.h b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.h index 072547b77ad7..f7abae534fca 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.h +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.h @@ -3,6 +3,10 @@ // found in the LICENSE file. #import +@class FIAPaymentQueueHandler; @interface InAppPurchasePlugin : NSObject + +@property(strong, nonatomic) FIAPaymentQueueHandler *paymentQueueHandler; + @end diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index efd85b031947..1d604c4540cd 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -6,6 +6,14 @@ #import #import "FIAObjectTranslator.h" #import "FIAPRequestHandler.h" +#import "FIAPaymentQueueHandler.h" + +typedef enum : NSUInteger { + PaymentQueueCallbackTypeUpdate, + PaymentQueueCallbackTypeRemoved, + PaymentQueueCallbackTypeRestoreTransactionFailed, + PaymentQueueCallbackTypeRestoreCompletedTransactionsFinished, +} PaymentQueueCallbackType; @interface InAppPurchasePlugin () @@ -13,6 +21,16 @@ @interface InAppPurchasePlugin () // the request is finished. @property(strong, nonatomic) NSMutableSet *requestHandlers; +// After querying the product, the available products will be saved in the map to be used +// for purchase. +@property(copy, nonatomic) NSDictionary *productsMap; + +// Call back channel to dart used for when a listener function is triggered. +@property(strong, nonatomic) FlutterMethodChannel *callbackChannel; +@property(strong, nonatomic) NSObject *registry; +@property(strong, nonatomic) NSObject *messenger; +@property(strong, nonatomic) NSObject *registrar; + @end @implementation InAppPurchasePlugin @@ -35,6 +53,38 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } } +- (instancetype)initWithRegistrar:(NSObject *)registrar { + self = [self init]; + self.registrar = registrar; + self.registry = [registrar textures]; + self.messenger = [registrar messenger]; + __weak typeof(self) weakSelf = self; + self.paymentQueueHandler = + [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueue defaultQueue] + transactionsUpdated:^(NSArray *_Nonnull transactions) { + [weakSelf handleTransactionsUpdated:transactions]; + } + transactionRemoved:^(NSArray *_Nonnull transactions) { + [weakSelf handleTransactionsRemoved:transactions]; + } + restoreTransactionFailed:^(NSError *_Nonnull error) { + [self handleTransactionRestoreFailed:error]; + } + restoreCompletedTransactionsFinished:^{ + [self restoreCompletedTransactionsFinished]; + } + shouldAddStorePayment:^BOOL(SKPayment *payment, SKProduct *product) { + return [self shouldAddStorePayment:payment product:product]; + } + updatedDownloads:^void(NSArray *_Nonnull downloads) { + [self updatedDownloads:downloads]; + }]; + self.callbackChannel = + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase_callback" + binaryMessenger:[registrar messenger]]; + return self; +} + - (void)canMakePayments:(FlutterResult)result { result([NSNumber numberWithBool:[SKPaymentQueue canMakePayments]]); } From 73caea71225ff2ba53cf2fb02be24d11214ed960 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 15 Feb 2019 10:34:22 -0800 Subject: [PATCH 02/64] second draft --- .../InAppPurchasePluginTest.m | 86 +++++++- .../PaymentQueueTest.m | 186 ++++++++++++++++++ .../ios/in_app_purchase_pluginTests/Stubs.h | 5 + .../ios/in_app_purchase_pluginTests/Stubs.m | 28 +++ .../ios/Classes/FIAPaymentQueueHandler.h | 41 ++++ .../ios/Classes/FIAPaymentQueueHandler.m | 120 +++++++++++ .../ios/Classes/InAppPurchasePlugin.m | 81 ++++++++ 7 files changed, 542 insertions(+), 5 deletions(-) create mode 100644 packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/PaymentQueueTest.m create mode 100644 packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h create mode 100644 packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m index 1d841868cefb..c1102cd74b3d 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m @@ -3,18 +3,20 @@ // found in the LICENSE file. #import +#import "FIAPaymentQueueHandler.h" #import "InAppPurchasePlugin.h" #import "Stubs.h" @interface InAppPurchasePluginTest : XCTestCase +@property(strong, nonatomic) InAppPurchasePlugin* plugin; + @end @implementation InAppPurchasePluginTest -InAppPurchasePlugin* plugin; - (void)setUp { - plugin = [[InAppPurchasePluginStub alloc] init]; + self.plugin = [[InAppPurchasePluginStub alloc] init]; } - (void)tearDown { @@ -25,7 +27,7 @@ - (void)testInvalidMethodCall { [self expectationWithDescription:@"expect result to be not implemented"]; FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"invalid" arguments:NULL]; __block id result; - [plugin handleMethodCall:call + [self.plugin handleMethodCall:call result:^(id r) { [expectation fulfill]; result = r; @@ -40,7 +42,7 @@ - (void)testCanMakePayments { [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue canMakePayments:]" arguments:NULL]; __block id result; - [plugin handleMethodCall:call + [self.plugin handleMethodCall:call result:^(id r) { [expectation fulfill]; result = r; @@ -56,7 +58,7 @@ - (void)testGetProductResponse { methodCallWithMethodName:@"-[InAppPurchasePlugin startProductRequest:result:]" arguments:@[ @"123" ]]; __block id result; - [plugin handleMethodCall:call + [self.plugin handleMethodCall:call result:^(id r) { [expectation fulfill]; result = r; @@ -68,4 +70,78 @@ - (void)testGetProductResponse { XCTAssertTrue([resultArray.firstObject[@"productIdentifier"] isEqualToString:@"123"]); } +- (void)testAddPaymentFailure { + XCTestExpectation* expectation = + [self expectationWithDescription:@"result should return failed state"]; + FlutterMethodCall* call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:@{ + @"productID" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandBox" : @YES + }]; + SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; + queue.testState = SKPaymentTransactionStateFailed; + __block SKPaymentTransaction* transactionForUpdateBlock; + self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray* _Nonnull transactions) { + SKPaymentTransaction* transaction = transactions[0]; + if (transaction.transactionState == SKPaymentTransactionStateFailed) { + transactionForUpdateBlock = transaction; + [expectation fulfill]; + } + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { + return YES; + } + updatedDownloads:nil]; + [queue addTransactionObserver:self.plugin.paymentQueueHandler]; + self.plugin.paymentQueueHandler.testing = YES; + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStateFailed); +} + +- (void)testAddPaymentSuccessWithMockQueue { + XCTestExpectation* expectation = + [self expectationWithDescription:@"result should return success state"]; + FlutterMethodCall* call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:@{ + @"productID" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandBox" : @YES + }]; + SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; + queue.testState = SKPaymentTransactionStatePurchased; + __block SKPaymentTransaction* transactionForUpdateBlock; + self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray* _Nonnull transactions) { + SKPaymentTransaction* transaction = transactions[0]; + if (transaction.transactionState == SKPaymentTransactionStatePurchased) { + transactionForUpdateBlock = transaction; + [expectation fulfill]; + } + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { + return YES; + } + updatedDownloads:nil]; + [queue addTransactionObserver:self.plugin.paymentQueueHandler]; + self.plugin.paymentQueueHandler.testing = YES; + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased); +} + @end diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/PaymentQueueTest.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/PaymentQueueTest.m new file mode 100644 index 000000000000..9fd0fdbe28d8 --- /dev/null +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/PaymentQueueTest.m @@ -0,0 +1,186 @@ +// Copyright 2019 The Chromium 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 +#import "FIAPaymentQueueHandler.h" +#import "Stubs.h" + +@interface PaymentQueueTest : XCTestCase + +@property(strong, nonatomic) NSDictionary *periodMap; +@property(strong, nonatomic) NSDictionary *discountMap; +@property(strong, nonatomic) NSDictionary *productMap; +@property(strong, nonatomic) NSDictionary *productResponseMap; + +@end + +@implementation PaymentQueueTest + +- (void)setUp { + self.periodMap = @{@"numberOfUnits" : @(0), @"unit" : @(0)}; + self.discountMap = @{ + @"price" : @1.0, + @"currencyCode" : @"USD", + @"numberOfPeriods" : @1, + @"subscriptionPeriod" : self.periodMap, + @"paymentMode" : @1 + }; + self.productMap = @{ + @"price" : @1.0, + @"currencyCode" : @"USD", + @"productIdentifier" : @"123", + @"localizedTitle" : @"title", + @"localizedDescription" : @"des", + @"downloadable" : @YES, + @"downloadContentLengths" : @1, + @"downloadContentVersion" : [NSNull null], // not mockable + @"subscriptionPeriod" : self.periodMap, + @"introductoryPrice" : self.discountMap, + @"subscriptionGroupIdentifier" : @"com.group" + }; + self.productResponseMap = + @{@"products" : @[ self.productMap ], @"invalidProductIdentifiers" : [NSNull null]}; +} + +- (void)testTransactionPurchased { + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect to get purchased transcation."]; + SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; + queue.testState = SKPaymentTransactionStatePurchased; + __block SKPaymentTransactionStub *tran; + FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + SKPaymentTransaction *transaction = transactions[0]; + tran = (SKPaymentTransactionStub *)transaction; + [expectation fulfill]; + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + updatedDownloads:nil]; + handler.testing = YES; + [queue addTransactionObserver:handler]; + SKPayment *payment = + [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; + [handler addPayment:payment]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(tran.transactionState, SKPaymentTransactionStatePurchased); +} + +- (void)testTransactionFailed { + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect to get failed transcation."]; + SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; + queue.testState = SKPaymentTransactionStateFailed; + __block SKPaymentTransactionStub *tran; + FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + SKPaymentTransaction *transaction = transactions[0]; + tran = (SKPaymentTransactionStub *)transaction; + [expectation fulfill]; + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + updatedDownloads:nil]; + handler.testing = YES; + [queue addTransactionObserver:handler]; + SKPayment *payment = + [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; + [handler addPayment:payment]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(tran.transactionState, SKPaymentTransactionStateFailed); +} + +- (void)testTransactionRestored { + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect to get restored transcation."]; + SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; + queue.testState = SKPaymentTransactionStateRestored; + __block SKPaymentTransactionStub *tran; + FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + SKPaymentTransaction *transaction = transactions[0]; + tran = (SKPaymentTransactionStub *)transaction; + [expectation fulfill]; + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + updatedDownloads:nil]; + handler.testing = YES; + [queue addTransactionObserver:handler]; + SKPayment *payment = + [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; + [handler addPayment:payment]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(tran.transactionState, SKPaymentTransactionStateRestored); +} + +- (void)testTransactionPurchasing { + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect to get purchasing transcation."]; + SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; + queue.testState = SKPaymentTransactionStatePurchasing; + __block SKPaymentTransactionStub *tran; + FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + SKPaymentTransaction *transaction = transactions[0]; + tran = (SKPaymentTransactionStub *)transaction; + [expectation fulfill]; + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + updatedDownloads:nil]; + handler.testing = YES; + [queue addTransactionObserver:handler]; + SKPayment *payment = + [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; + [handler addPayment:payment]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(tran.transactionState, SKPaymentTransactionStatePurchasing); +} + +- (void)testTransactionDeferred { + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect to get deffered transcation."]; + SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; + queue.testState = SKPaymentTransactionStateDeferred; + __block SKPaymentTransactionStub *tran; + FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + SKPaymentTransaction *transaction = transactions[0]; + tran = (SKPaymentTransactionStub *)transaction; + [expectation fulfill]; + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + updatedDownloads:nil]; + handler.testing = YES; + [queue addTransactionObserver:handler]; + SKPayment *payment = + [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; + [handler addPayment:payment]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(tran.transactionState, SKPaymentTransactionStateDeferred); +} + +@end diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.h b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.h index e194e450a33e..9ea6e6a54868 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.h +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.h @@ -32,8 +32,13 @@ NS_ASSUME_NONNULL_BEGIN @interface InAppPurchasePluginStub : InAppPurchasePlugin @end +@interface SKPaymentQueueStub : SKPaymentQueue +@property(assign, nonatomic) SKPaymentTransactionState testState; +@end + @interface SKPaymentTransactionStub : SKPaymentTransaction - (instancetype)initWithMap:(NSDictionary *)map; +- (instancetype)initWithState:(SKPaymentTransactionState)state; @end @interface SKDownloadStub : SKDownload diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m index 39defa632c06..c24facf66587 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m @@ -130,6 +130,26 @@ - (SKProductRequestStub *)getProductRequestWithIdentifiers:(NSSet *)identifiers @end +@interface SKPaymentQueueStub () + +@property(strong, nonatomic) id observer; + +@end + +@implementation SKPaymentQueueStub + +- (void)addTransactionObserver:(id)observer { + self.observer = observer; +} + +- (void)addPayment:(SKPayment *)payment { + SKPaymentTransactionStub *transaction = + [[SKPaymentTransactionStub alloc] initWithState:self.testState]; + [self.observer paymentQueue:self updatedTransactions:@[ transaction ]]; +} + +@end + @implementation SKPaymentTransactionStub - (instancetype)initWithID:(NSString *)identifier { @@ -163,6 +183,14 @@ - (instancetype)initWithMap:(NSDictionary *)map { return self; } +- (instancetype)initWithState:(SKPaymentTransactionState)state { + self = [super init]; + if (self) { + [self setValue:@(state) forKey:@"transactionState"]; + } + return self; +} + @end @implementation SKDownloadStub diff --git a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h new file mode 100644 index 000000000000..952e687c402b --- /dev/null +++ b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h @@ -0,0 +1,41 @@ +// Copyright 2019 The Chromium 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 +#import + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const TestingProductID; + +typedef void (^TransactionsUpdated)(NSArray *transactions); +typedef void (^TransactionsRemoved)(NSArray *transactions); +typedef void (^RestoreTransactionFailed)(NSError *error); +typedef void (^RestoreCompletedTransactionsFinished)(void); +typedef BOOL (^ShouldAddStorePayment)(SKPayment *payment, SKProduct *product); +typedef void (^UpdatedDownloads)(NSArray *downloads); + +@interface FIAPaymentQueueHandler : NSObject + +- (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; +- (void)addPayment:(SKPayment *)payment; + +// Enable testing. + +// Because payment object in transaction is not KVC, we cannot mock the payment object in the +// transaction. So there is no easay way to create stubs and test the handler. +// When set to true, we always this a constant TestingProductID as product ID +// when storing and accessing the completion block from self.completionMap +@property(assign, nonatomic) BOOL testing; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m new file mode 100644 index 000000000000..0f74cb596f9b --- /dev/null +++ b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m @@ -0,0 +1,120 @@ +// Copyright 2019 The Chromium 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" + +NSString *const TestingProductID = @"testing"; + +@interface FIAPaymentQueueHandler () + +@property(strong, nonatomic) SKPaymentQueue *queue; +@property(nullable, copy, nonatomic) TransactionsUpdated transactionsUpdated; +@property(nullable, copy, nonatomic) TransactionsRemoved transactionsRemoved; +@property(nullable, copy, nonatomic) RestoreTransactionFailed restoreTransactionFailed; +@property(nullable, copy, nonatomic) + RestoreCompletedTransactionsFinished paymentQueueRestoreCompletedTransactionsFinished; +@property(nullable, copy, nonatomic) ShouldAddStorePayment shouldAddStorePayment; +@property(nullable, copy, nonatomic) UpdatedDownloads updatedDownloads; + +@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 { + self = [super init]; + if (self) { + self.queue = queue; + self.transactionsUpdated = transactionsUpdated; + self.transactionsRemoved = transactionsRemoved; + self.restoreTransactionFailed = restoreTransactionFailed; + self.paymentQueueRestoreCompletedTransactionsFinished = restoreCompletedTransactionsFinished; + self.shouldAddStorePayment = shouldAddStorePayment; + self.updatedDownloads = updatedDownloads; + } + return self; +} + +- (void)addPayment:(SKPayment *)payment { + NSString *productID = payment.productIdentifier; + if (self.testing) { + productID = TestingProductID; + } + [self.queue addPayment:payment]; +} + +#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 *)transactions { + // notify dart through callbacks. + if (self.transactionsUpdated) { + self.transactionsUpdated(transactions); + } + for (SKPaymentTransaction *transaction in transactions) { + switch (transaction.transactionState) { + // The following three states indicates that the transaction has been complete. + // We mark the transaction to be finished and send the signal back to dart. + case SKPaymentTransactionStatePurchased: + case SKPaymentTransactionStateFailed: + case SKPaymentTransactionStateRestored: + // mark finished transaction as finished as required by OBJC api. + [queue finishTransaction:transaction]; + break; + default: + break; + } + } +} + +// Sent when transactions are removed from the queue (via finishTransaction:). +- (void)paymentQueue:(SKPaymentQueue *)queue + removedTransactions:(NSArray *)transactions { + if (self.transactionsRemoved) { + 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 { + if (self.restoreTransactionFailed) { + 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 { + if (self.paymentQueueRestoreCompletedTransactionsFinished) { + self.paymentQueueRestoreCompletedTransactionsFinished(); + } +} + +// Sent when the download state has changed. +- (void)paymentQueue:(SKPaymentQueue *)queue updatedDownloads:(NSArray *)downloads { + if (self.updatedDownloads) { + 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 { + if (self.shouldAddStorePayment) { + return (self.shouldAddStorePayment(payment, product)); + } + return YES; +} + +@end diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index 1d604c4540cd..eb1963851192 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -48,6 +48,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [self canMakePayments:result]; } else if ([@"-[InAppPurchasePlugin startProductRequest:result:]" isEqualToString:call.method]) { [self handleProductRequestMethodCall:call result:result]; + } else if ([@"-[InAppPurchasePlugin addPayment:result:]" isEqualToString:call.method]) { + [self addPayment:call result:result]; } else { result(FlutterMethodNotImplemented); } @@ -125,6 +127,85 @@ - (void)handleProductRequestMethodCall:(FlutterMethodCall *)call result:(Flutter }]; } +- (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { + if (![call.arguments isKindOfClass:[NSDictionary class]]) { + result([FlutterError errorWithCode:@"storekit_invalide_argument" + message:@"Argument type of addPayment is not a map" + details:call.arguments]); + return; + } + NSDictionary *paymentMap = (NSDictionary *)call.arguments; + NSString *productID = [paymentMap objectForKey:@"productID"]; + SKProduct *product = [self.productsMap objectForKey:productID]; + if ([[paymentMap objectForKey:@"mutable"] boolValue]) { + SKMutablePayment *payment = [[SKMutablePayment alloc] init]; + payment.productIdentifier = productID; + NSNumber *quantity = [paymentMap objectForKey:@"quantity"]; + if (quantity) { + payment.quantity = quantity.integerValue; + } + NSString *applicationUsername = [paymentMap objectForKey:@"applicationUsername"]; + payment.applicationUsername = applicationUsername; + if (@available(iOS 8.3, *)) { + payment.simulatesAskToBuyInSandbox = + [[paymentMap objectForKey:@"simulatesAskToBuyInSandBox"] boolValue]; + } else { + // Fallback on earlier versions + } + [self.paymentQueueHandler addPayment:payment]; + } else { + SKPayment *payment = [SKPayment paymentWithProduct:product]; + [self.paymentQueueHandler addPayment:payment]; + } +} + +#pragma mark - delegates + +- (void)handleTransactionsUpdated:(NSArray *)transactions { + NSMutableArray *maps = [NSMutableArray new]; + for (SKPaymentTransaction *transcation in transactions) { + [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transcation]]; + } + [self.callbackChannel invokeMethod:@"updatedTransaction" arguments:maps]; +} + +- (void)handleTransactionsRemoved:(NSArray *)transactions { + NSMutableArray *maps = [NSMutableArray new]; + for (SKPaymentTransaction *transcation in transactions) { + [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transcation]]; + } + [self.callbackChannel invokeMethod:@"removedTransaction" arguments:maps]; +} + +- (void)handleTransactionRestoreFailed:(NSError *)error { + FlutterError *fltError = [FlutterError errorWithCode:error.domain + message:error.description + details:error.description]; + [self.callbackChannel invokeMethod:@"restoreCompletedTransactions" arguments:fltError]; +} + +- (void)restoreCompletedTransactionsFinished { + [self.callbackChannel invokeMethod:@"paymentQueueRestoreCompletedTransactionsFinished" + arguments:nil]; +} + +- (void)updatedDownloads:(NSArray *)downloads { + NSMutableArray *maps = [NSMutableArray new]; + for (SKDownload *download in downloads) { + [maps addObject:[FIAObjectTranslator getMapFromSKDownload:download]]; + } + [self.callbackChannel invokeMethod:@"updatedDownloads" arguments:maps]; +} + +- (BOOL)shouldAddStorePayment:(SKPayment *)payment product:(SKProduct *)product { + // TODO(cyanglaz): getting the callback from dart so dart is able to override this callback. + // Currently the invokeMethod gets result asynchronously and dispatch_semaphore does not work + // with the invokeMethod api. + return YES; +} + +#pragma mark - dependency injection (for unit testing) + - (SKProductsRequest *)getProductRequestWithIdentifiers:(NSSet *)identifiers { return [[SKProductsRequest alloc] initWithProductIdentifiers:identifiers]; } From 9420db0fdcf6a9835128c8c5cca8cadb2b4431d6 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 15 Feb 2019 11:14:30 -0800 Subject: [PATCH 03/64] finish transaction --- .../ios/Classes/FIAPaymentQueueHandler.h | 3 ++ .../ios/Classes/FIAPaymentQueueHandler.m | 34 +++++++++++-------- .../ios/Classes/InAppPurchasePlugin.m | 29 +++++++++++++++- 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h index 952e687c402b..8c2ec313ac37 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h +++ b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h @@ -18,6 +18,8 @@ typedef void (^UpdatedDownloads)(NSArray *downloads); @interface FIAPaymentQueueHandler : NSObject +@property (copy, nonatomic, readonly) NSDictionary *transactions; + - (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated transactionRemoved:(nullable TransactionsRemoved)transactionsRemoved @@ -27,6 +29,7 @@ typedef void (^UpdatedDownloads)(NSArray *downloads); shouldAddStorePayment:(nullable ShouldAddStorePayment)shouldAddStorePayment updatedDownloads:(nullable UpdatedDownloads)updatedDownloads; - (void)addPayment:(SKPayment *)payment; +- (void)finishTransaction:(SKPaymentTransaction *)transaction; // Enable testing. diff --git a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m index 0f74cb596f9b..51b2bf512d5d 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m +++ b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m @@ -17,6 +17,8 @@ @interface FIAPaymentQueueHandler () @property(nullable, copy, nonatomic) ShouldAddStorePayment shouldAddStorePayment; @property(nullable, copy, nonatomic) UpdatedDownloads updatedDownloads; +@property (strong, nonatomic) NSMutableDictionary *transactionsSetter; + @end @implementation FIAPaymentQueueHandler @@ -38,6 +40,7 @@ - (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue self.paymentQueueRestoreCompletedTransactionsFinished = restoreCompletedTransactionsFinished; self.shouldAddStorePayment = shouldAddStorePayment; self.updatedDownloads = updatedDownloads; + self.transactionsSetter = [NSMutableDictionary new]; } return self; } @@ -50,29 +53,26 @@ - (void)addPayment:(SKPayment *)payment { [self.queue addPayment:payment]; } +- (void)finishTransaction:(SKPaymentTransaction *)transaction { + @try { + [self.queue finishTransaction:transaction]; + } @catch (NSException *e) { + [e raise]; + } +} + #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 *)transactions { + for (SKPaymentTransaction *transaction in transactions) { + [self.transactionsSetter setObject:transaction forKey:transaction.transactionIdentifier]; + } // notify dart through callbacks. if (self.transactionsUpdated) { self.transactionsUpdated(transactions); } - for (SKPaymentTransaction *transaction in transactions) { - switch (transaction.transactionState) { - // The following three states indicates that the transaction has been complete. - // We mark the transaction to be finished and send the signal back to dart. - case SKPaymentTransactionStatePurchased: - case SKPaymentTransactionStateFailed: - case SKPaymentTransactionStateRestored: - // mark finished transaction as finished as required by OBJC api. - [queue finishTransaction:transaction]; - break; - default: - break; - } - } } // Sent when transactions are removed from the queue (via finishTransaction:). @@ -117,4 +117,10 @@ - (BOOL)paymentQueue:(SKPaymentQueue *)queue return YES; } +#pragma mark - getter + +- (NSDictionary *)transactions { + return self.transactionsSetter; +} + @end diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index eb1963851192..fbb763a0a76b 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -49,7 +49,9 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } else if ([@"-[InAppPurchasePlugin startProductRequest:result:]" isEqualToString:call.method]) { [self handleProductRequestMethodCall:call result:result]; } else if ([@"-[InAppPurchasePlugin addPayment:result:]" isEqualToString:call.method]) { - [self addPayment:call result:result]; + [self addPayment:call result:result]; + } else if ([@"-[InAppPurchasePlugin finishTransaction]" isEqualToString:call.method]) { + [self finishTransaction:call result:result]; } else { result(FlutterMethodNotImplemented); } @@ -159,6 +161,31 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { } } +- (void)finishTransaction:(FlutterMethodCall *)call result:(FlutterResult)result { + if (![call.arguments isKindOfClass:[NSString class]]) { + result([FlutterError errorWithCode:@"storekit_invalide_argument" + message:@"Argument type of addPayment is not a string" + details:call.arguments]); + return; + } + NSString *identifier = call.arguments; + SKPaymentTransaction *transaction = [self.paymentQueueHandler.transactions objectForKey:identifier]; + if (!transaction) { + result([FlutterError errorWithCode:@"storekit_platform_invalid_transaction" + message:@"Invalid transaction ID is used." + details:call.arguments]); + return; + } + @try { + [self.paymentQueueHandler finishTransaction:transaction]; + } @catch (NSException *e){ + result([FlutterError errorWithCode:@"storekit_finish_transaction_exception" + message:e.name + details:e.description]); + return; + } +} + #pragma mark - delegates - (void)handleTransactionsUpdated:(NSArray *)transactions { From 62a99740daa8fed7479f8086effa83817941bdcb Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 15 Feb 2019 11:16:31 -0800 Subject: [PATCH 04/64] adding more comments --- packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index fbb763a0a76b..915106bddb34 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -139,6 +139,7 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { NSDictionary *paymentMap = (NSDictionary *)call.arguments; NSString *productID = [paymentMap objectForKey:@"productID"]; SKProduct *product = [self.productsMap objectForKey:productID]; + // User can use payment object with mutable = true and add simulatesAskToBuyInSandBox = true to test the payment flow. if ([[paymentMap objectForKey:@"mutable"] boolValue]) { SKMutablePayment *payment = [[SKMutablePayment alloc] init]; payment.productIdentifier = productID; From 2ec9c58918f7af52511a4afc08a47e528d563f86 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 15 Feb 2019 11:21:23 -0800 Subject: [PATCH 05/64] more comments --- packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h | 1 + packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m | 1 + packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m | 1 + 3 files changed, 3 insertions(+) diff --git a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h index 8c2ec313ac37..bee0af7448e5 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h +++ b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h @@ -29,6 +29,7 @@ typedef void (^UpdatedDownloads)(NSArray *downloads); shouldAddStorePayment:(nullable ShouldAddStorePayment)shouldAddStorePayment updatedDownloads:(nullable UpdatedDownloads)updatedDownloads; - (void)addPayment:(SKPayment *)payment; +// Can throw exceptions, should always used in a @try block. - (void)finishTransaction:(SKPaymentTransaction *)transaction; // Enable testing. diff --git a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m index 51b2bf512d5d..a051b62c880a 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m +++ b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m @@ -57,6 +57,7 @@ - (void)finishTransaction:(SKPaymentTransaction *)transaction { @try { [self.queue finishTransaction:transaction]; } @catch (NSException *e) { + // finish transaction will throw exception if the transaction type is purchasing. Raise the exception so the InAppPurchasePlugin will get notified. [e raise]; } } diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index 915106bddb34..aacc2eb22c1b 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -178,6 +178,7 @@ - (void)finishTransaction:(FlutterMethodCall *)call result:(FlutterResult)result return; } @try { + // finish transaction will throw exception if the transaction type is purchasing. Notify dart about this exception. [self.paymentQueueHandler finishTransaction:transaction]; } @catch (NSException *e){ result([FlutterError errorWithCode:@"storekit_finish_transaction_exception" From e75411a4e5406cf05105353d035abe9eaa3ec606 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 15 Feb 2019 11:26:59 -0800 Subject: [PATCH 06/64] let dart intercept the payment from store --- .../in_app_purchase/ios/Classes/InAppPurchasePlugin.m | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index aacc2eb22c1b..5884ff02a818 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -227,10 +227,11 @@ - (void)updatedDownloads:(NSArray *)downloads { } - (BOOL)shouldAddStorePayment:(SKPayment *)payment product:(SKProduct *)product { - // TODO(cyanglaz): getting the callback from dart so dart is able to override this callback. - // Currently the invokeMethod gets result asynchronously and dispatch_semaphore does not work - // with the invokeMethod api. - return YES; + // We alwasy return NO here. And we send the message to dart to process the payment; and we will have a incerpection method that deciding if the payment should be processed (implemented by the programmer). + [self.callbackChannel invokeMethod:@"shouldAddStorePayment" arguments:@{@"payment":[FIAObjectTranslator getMapFromSKPayment:payment], + @"proudt":[FIAObjectTranslator getMapFromSKProduct:product] + }]; + return NO; } #pragma mark - dependency injection (for unit testing) From 824eee86045d41ffe99dcb1b87fe29150f37791d Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 15 Feb 2019 12:59:47 -0800 Subject: [PATCH 07/64] create payment method --- .../InAppPurchasePluginTest.m | 22 +++++++ .../ios/in_app_purchase_pluginTests/Stubs.m | 1 + .../ios/Classes/InAppPurchasePlugin.m | 66 +++++++++++++++---- 3 files changed, 76 insertions(+), 13 deletions(-) diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m index c1102cd74b3d..3bf1c5f6cfe6 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m @@ -70,6 +70,28 @@ - (void)testGetProductResponse { XCTAssertTrue([resultArray.firstObject[@"productIdentifier"] isEqualToString:@"123"]); } +- (void)testCreatePaymentWithProduct { + XCTestExpectation* expectation = + [self expectationWithDescription:@"must return a payment"]; + FlutterMethodCall* call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin startProductRequest:result:]" + arguments:@[ @"123" ]]; + FlutterMethodCall* createPaymentCall = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin createPaymentWithProductID:result:]" + arguments:@"123"]; + __block NSDictionary *result; + __weak typeof(self) weakSelf = self; + [self.plugin handleMethodCall:call + result:^(id queryResult) { + [weakSelf.plugin handleMethodCall:createPaymentCall result:^(id _Nullable r) { + result = r; + [expectation fulfill]; + }]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertTrue([result[@"productIdentifier"] isEqualToString:@"123"]); +} + - (void)testAddPaymentFailure { XCTestExpectation* expectation = [self expectationWithDescription:@"result should return failed state"]; diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m index c24facf66587..e5fff5a977a0 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m @@ -186,6 +186,7 @@ - (instancetype)initWithMap:(NSDictionary *)map { - (instancetype)initWithState:(SKPaymentTransactionState)state { self = [super init]; if (self) { + [self setValue:@"fakeID" forKey:@"transactionIdentifier"]; [self setValue:@(state) forKey:@"transactionState"]; } return self; diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index 5884ff02a818..e290e2808995 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -23,7 +23,9 @@ @interface InAppPurchasePlugin () // After querying the product, the available products will be saved in the map to be used // for purchase. -@property(copy, nonatomic) NSDictionary *productsMap; +@property(copy, nonatomic) NSMutableDictionary *productsCache; +// Saved payment object used for resume payments; +@property(copy, nonatomic) NSMutableDictionary *paymentsCache;; // Call back channel to dart used for when a listener function is triggered. @property(strong, nonatomic) FlutterMethodChannel *callbackChannel; @@ -50,8 +52,10 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [self handleProductRequestMethodCall:call result:result]; } else if ([@"-[InAppPurchasePlugin addPayment:result:]" isEqualToString:call.method]) { [self addPayment:call result:result]; - } else if ([@"-[InAppPurchasePlugin finishTransaction]" isEqualToString:call.method]) { + } else if ([@"-[InAppPurchasePlugin finishTransaction:result:]" isEqualToString:call.method]) { [self finishTransaction:call result:result]; + } else if ([@"-[InAppPurchasePlugin createPaymentWithProductID:result:]" isEqualToString:call.method]) { + [self createPaymentWithProductID:call result:result]; } else { result(FlutterMethodNotImplemented); } @@ -124,11 +128,34 @@ - (void)handleProductRequestMethodCall:(FlutterMethodCall *)call result:(Flutter details:call.arguments]); return; } + for (SKProduct *product in response.products) { + [self.productsCache setObject:product forKey:product.productIdentifier]; + } result([FIAObjectTranslator getMapFromSKProductsResponse:response]); [weakSelf.requestHandlers removeObject:handler]; }]; } +- (void)createPaymentWithProductID:(FlutterMethodCall *)call result:(FlutterResult)result { + if (![call.arguments isKindOfClass:[NSString class]]) { + result([FlutterError errorWithCode:@"storekit_invalide_argument" + message:@"Argument type of createPaymentWithProductID is not a string." + details:call.arguments]); + return; + } + NSString *productID = call.arguments; + SKProduct *product = [self.productsCache objectForKey:productID]; + if (!product) { + result([FlutterError errorWithCode:@"storekit_product_not_found" + message:@"Cannot find the product. To create a payment of a product, you must query the product with SKProductRequestMaker.startProductRequest." + details:call.arguments]); + return; + } + SKPayment *payment = [SKPayment paymentWithProduct:product]; + [self.paymentsCache setObject:payment forKey:productID]; + result([FIAObjectTranslator getMapFromSKPayment:payment]); +} + - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { if (![call.arguments isKindOfClass:[NSDictionary class]]) { result([FlutterError errorWithCode:@"storekit_invalide_argument" @@ -138,26 +165,23 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { } NSDictionary *paymentMap = (NSDictionary *)call.arguments; NSString *productID = [paymentMap objectForKey:@"productID"]; - SKProduct *product = [self.productsMap objectForKey:productID]; + SKPayment *payment = [self.paymentsCache objectForKey:productID]; // User can use payment object with mutable = true and add simulatesAskToBuyInSandBox = true to test the payment flow. - if ([[paymentMap objectForKey:@"mutable"] boolValue]) { - SKMutablePayment *payment = [[SKMutablePayment alloc] init]; - payment.productIdentifier = productID; + if (!payment || [paymentMap[@"mutable"] boolValue] == YES) { + SKMutablePayment *mutablePayment = [[SKMutablePayment alloc] init]; + mutablePayment.productIdentifier = productID; NSNumber *quantity = [paymentMap objectForKey:@"quantity"]; if (quantity) { - payment.quantity = quantity.integerValue; + mutablePayment.quantity = quantity.integerValue; } NSString *applicationUsername = [paymentMap objectForKey:@"applicationUsername"]; - payment.applicationUsername = applicationUsername; + mutablePayment.applicationUsername = applicationUsername; if (@available(iOS 8.3, *)) { - payment.simulatesAskToBuyInSandbox = + mutablePayment.simulatesAskToBuyInSandbox = [[paymentMap objectForKey:@"simulatesAskToBuyInSandBox"] boolValue]; - } else { - // Fallback on earlier versions } [self.paymentQueueHandler addPayment:payment]; } else { - SKPayment *payment = [SKPayment paymentWithProduct:product]; [self.paymentQueueHandler addPayment:payment]; } } @@ -165,7 +189,7 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { - (void)finishTransaction:(FlutterMethodCall *)call result:(FlutterResult)result { if (![call.arguments isKindOfClass:[NSString class]]) { result([FlutterError errorWithCode:@"storekit_invalide_argument" - message:@"Argument type of addPayment is not a string" + message:@"Argument type of finishTransaction is not a string." details:call.arguments]); return; } @@ -228,6 +252,8 @@ - (void)updatedDownloads:(NSArray *)downloads { - (BOOL)shouldAddStorePayment:(SKPayment *)payment product:(SKProduct *)product { // We alwasy return NO here. And we send the message to dart to process the payment; and we will have a incerpection method that deciding if the payment should be processed (implemented by the programmer). + [self.productsCache setObject:product forKey:product.productIdentifier]; + [self.paymentsCache setObject:payment forKey:payment.productIdentifier]; [self.callbackChannel invokeMethod:@"shouldAddStorePayment" arguments:@{@"payment":[FIAObjectTranslator getMapFromSKPayment:payment], @"proudt":[FIAObjectTranslator getMapFromSKProduct:product] }]; @@ -249,4 +275,18 @@ - (NSSet *)requestHandlers { return _requestHandlers; } +- (NSMutableDictionary *)productsCache { + if (!_productsCache) { + _productsCache = [NSMutableDictionary new]; + } + return _productsCache; +} + +- (NSMutableDictionary *)paymentsCache { + if (!_paymentsCache) { + _paymentsCache = [NSMutableDictionary new]; + } + return _paymentsCache; +} + @end From 98f8d034128825fac2c42fdc67cec47ff39bd3fa Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 15 Feb 2019 13:00:14 -0800 Subject: [PATCH 08/64] rearrange method order --- packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index e290e2808995..ae3004bad132 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -50,12 +50,12 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [self canMakePayments:result]; } else if ([@"-[InAppPurchasePlugin startProductRequest:result:]" isEqualToString:call.method]) { [self handleProductRequestMethodCall:call result:result]; + } else if ([@"-[InAppPurchasePlugin createPaymentWithProductID:result:]" isEqualToString:call.method]) { + [self createPaymentWithProductID:call result:result]; } else if ([@"-[InAppPurchasePlugin addPayment:result:]" isEqualToString:call.method]) { [self addPayment:call result:result]; } else if ([@"-[InAppPurchasePlugin finishTransaction:result:]" isEqualToString:call.method]) { [self finishTransaction:call result:result]; - } else if ([@"-[InAppPurchasePlugin createPaymentWithProductID:result:]" isEqualToString:call.method]) { - [self createPaymentWithProductID:call result:result]; } else { result(FlutterMethodNotImplemented); } From e1176e52852a93f50645359988fa11d88fa25036 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 15 Feb 2019 13:00:51 -0800 Subject: [PATCH 09/64] formatting --- .../InAppPurchasePluginTest.m | 198 ++++++------ .../ios/in_app_purchase_pluginTests/Stubs.m | 20 +- .../ios/Classes/FIAPaymentQueueHandler.h | 2 +- .../ios/Classes/FIAPaymentQueueHandler.m | 23 +- .../ios/Classes/InAppPurchasePlugin.m | 290 +++++++++--------- 5 files changed, 273 insertions(+), 260 deletions(-) diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m index 3bf1c5f6cfe6..5a061bcb3d2e 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m @@ -28,10 +28,10 @@ - (void)testInvalidMethodCall { FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"invalid" arguments:NULL]; __block id result; [self.plugin handleMethodCall:call - result:^(id r) { - [expectation fulfill]; - result = r; - }]; + result:^(id r) { + [expectation fulfill]; + result = r; + }]; [self waitForExpectations:@[ expectation ] timeout:5]; XCTAssertEqual(result, FlutterMethodNotImplemented); } @@ -43,10 +43,10 @@ - (void)testCanMakePayments { arguments:NULL]; __block id result; [self.plugin handleMethodCall:call - result:^(id r) { - [expectation fulfill]; - result = r; - }]; + result:^(id r) { + [expectation fulfill]; + result = r; + }]; [self waitForExpectations:@[ expectation ] timeout:5]; XCTAssertEqual(result, [NSNumber numberWithBool:YES]); } @@ -59,10 +59,10 @@ - (void)testGetProductResponse { arguments:@[ @"123" ]]; __block id result; [self.plugin handleMethodCall:call - result:^(id r) { - [expectation fulfill]; - result = r; - }]; + result:^(id r) { + [expectation fulfill]; + result = r; + }]; [self waitForExpectations:@[ expectation ] timeout:5]; XCTAssert([result isKindOfClass:[NSDictionary class]]); NSArray* resultArray = [result objectForKey:@"products"]; @@ -71,99 +71,99 @@ - (void)testGetProductResponse { } - (void)testCreatePaymentWithProduct { - XCTestExpectation* expectation = - [self expectationWithDescription:@"must return a payment"]; - FlutterMethodCall* call = [FlutterMethodCall - methodCallWithMethodName:@"-[InAppPurchasePlugin startProductRequest:result:]" - arguments:@[ @"123" ]]; - FlutterMethodCall* createPaymentCall = [FlutterMethodCall - methodCallWithMethodName:@"-[InAppPurchasePlugin createPaymentWithProductID:result:]" - arguments:@"123"]; - __block NSDictionary *result; - __weak typeof(self) weakSelf = self; - [self.plugin handleMethodCall:call - result:^(id queryResult) { - [weakSelf.plugin handleMethodCall:createPaymentCall result:^(id _Nullable r) { - result = r; - [expectation fulfill]; - }]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertTrue([result[@"productIdentifier"] isEqualToString:@"123"]); + XCTestExpectation* expectation = [self expectationWithDescription:@"must return a payment"]; + FlutterMethodCall* call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin startProductRequest:result:]" + arguments:@[ @"123" ]]; + FlutterMethodCall* createPaymentCall = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin createPaymentWithProductID:result:]" + arguments:@"123"]; + __block NSDictionary* result; + __weak typeof(self) weakSelf = self; + [self.plugin handleMethodCall:call + result:^(id queryResult) { + [weakSelf.plugin handleMethodCall:createPaymentCall + result:^(id _Nullable r) { + result = r; + [expectation fulfill]; + }]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertTrue([result[@"productIdentifier"] isEqualToString:@"123"]); } - (void)testAddPaymentFailure { - XCTestExpectation* expectation = - [self expectationWithDescription:@"result should return failed state"]; - FlutterMethodCall* call = - [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" - arguments:@{ - @"productID" : @"123", - @"quantity" : @(1), - @"simulatesAskToBuyInSandBox" : @YES - }]; - SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; - queue.testState = SKPaymentTransactionStateFailed; - __block SKPaymentTransaction* transactionForUpdateBlock; - self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray* _Nonnull transactions) { - SKPaymentTransaction* transaction = transactions[0]; - if (transaction.transactionState == SKPaymentTransactionStateFailed) { - transactionForUpdateBlock = transaction; - [expectation fulfill]; - } - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { - return YES; - } - updatedDownloads:nil]; - [queue addTransactionObserver:self.plugin.paymentQueueHandler]; - self.plugin.paymentQueueHandler.testing = YES; - [self.plugin handleMethodCall:call - result:^(id r){ - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStateFailed); + XCTestExpectation* expectation = + [self expectationWithDescription:@"result should return failed state"]; + FlutterMethodCall* call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:@{ + @"productID" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandBox" : @YES + }]; + SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; + queue.testState = SKPaymentTransactionStateFailed; + __block SKPaymentTransaction* transactionForUpdateBlock; + self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray* _Nonnull transactions) { + SKPaymentTransaction* transaction = transactions[0]; + if (transaction.transactionState == SKPaymentTransactionStateFailed) { + transactionForUpdateBlock = transaction; + [expectation fulfill]; + } + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { + return YES; + } + updatedDownloads:nil]; + [queue addTransactionObserver:self.plugin.paymentQueueHandler]; + self.plugin.paymentQueueHandler.testing = YES; + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStateFailed); } - (void)testAddPaymentSuccessWithMockQueue { - XCTestExpectation* expectation = - [self expectationWithDescription:@"result should return success state"]; - FlutterMethodCall* call = - [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" - arguments:@{ - @"productID" : @"123", - @"quantity" : @(1), - @"simulatesAskToBuyInSandBox" : @YES - }]; - SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; - queue.testState = SKPaymentTransactionStatePurchased; - __block SKPaymentTransaction* transactionForUpdateBlock; - self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray* _Nonnull transactions) { - SKPaymentTransaction* transaction = transactions[0]; - if (transaction.transactionState == SKPaymentTransactionStatePurchased) { - transactionForUpdateBlock = transaction; - [expectation fulfill]; - } - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { - return YES; - } - updatedDownloads:nil]; - [queue addTransactionObserver:self.plugin.paymentQueueHandler]; - self.plugin.paymentQueueHandler.testing = YES; - [self.plugin handleMethodCall:call - result:^(id r){ - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased); + XCTestExpectation* expectation = + [self expectationWithDescription:@"result should return success state"]; + FlutterMethodCall* call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:@{ + @"productID" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandBox" : @YES + }]; + SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; + queue.testState = SKPaymentTransactionStatePurchased; + __block SKPaymentTransaction* transactionForUpdateBlock; + self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray* _Nonnull transactions) { + SKPaymentTransaction* transaction = transactions[0]; + if (transaction.transactionState == SKPaymentTransactionStatePurchased) { + transactionForUpdateBlock = transaction; + [expectation fulfill]; + } + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { + return YES; + } + updatedDownloads:nil]; + [queue addTransactionObserver:self.plugin.paymentQueueHandler]; + self.plugin.paymentQueueHandler.testing = YES; + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased); } @end diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m index e5fff5a977a0..2c4378ee4081 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m @@ -139,13 +139,13 @@ @interface SKPaymentQueueStub () @implementation SKPaymentQueueStub - (void)addTransactionObserver:(id)observer { - self.observer = observer; + self.observer = observer; } - (void)addPayment:(SKPayment *)payment { - SKPaymentTransactionStub *transaction = - [[SKPaymentTransactionStub alloc] initWithState:self.testState]; - [self.observer paymentQueue:self updatedTransactions:@[ transaction ]]; + SKPaymentTransactionStub *transaction = + [[SKPaymentTransactionStub alloc] initWithState:self.testState]; + [self.observer paymentQueue:self updatedTransactions:@[ transaction ]]; } @end @@ -184,12 +184,12 @@ - (instancetype)initWithMap:(NSDictionary *)map { } - (instancetype)initWithState:(SKPaymentTransactionState)state { - self = [super init]; - if (self) { - [self setValue:@"fakeID" forKey:@"transactionIdentifier"]; - [self setValue:@(state) forKey:@"transactionState"]; - } - return self; + self = [super init]; + if (self) { + [self setValue:@"fakeID" forKey:@"transactionIdentifier"]; + [self setValue:@(state) forKey:@"transactionState"]; + } + return self; } @end diff --git a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h index bee0af7448e5..9749fb57abe5 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h +++ b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h @@ -18,7 +18,7 @@ typedef void (^UpdatedDownloads)(NSArray *downloads); @interface FIAPaymentQueueHandler : NSObject -@property (copy, nonatomic, readonly) NSDictionary *transactions; +@property(copy, nonatomic, readonly) NSDictionary *transactions; - (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated diff --git a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m index a051b62c880a..ea4be4c507eb 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m +++ b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m @@ -17,7 +17,7 @@ @interface FIAPaymentQueueHandler () @property(nullable, copy, nonatomic) ShouldAddStorePayment shouldAddStorePayment; @property(nullable, copy, nonatomic) UpdatedDownloads updatedDownloads; -@property (strong, nonatomic) NSMutableDictionary *transactionsSetter; +@property(strong, nonatomic) NSMutableDictionary *transactionsSetter; @end @@ -54,12 +54,13 @@ - (void)addPayment:(SKPayment *)payment { } - (void)finishTransaction:(SKPaymentTransaction *)transaction { - @try { - [self.queue finishTransaction:transaction]; - } @catch (NSException *e) { - // finish transaction will throw exception if the transaction type is purchasing. Raise the exception so the InAppPurchasePlugin will get notified. - [e raise]; - } + @try { + [self.queue finishTransaction:transaction]; + } @catch (NSException *e) { + // finish transaction will throw exception if the transaction type is purchasing. Raise the + // exception so the InAppPurchasePlugin will get notified. + [e raise]; + } } #pragma mark - observing @@ -67,9 +68,9 @@ - (void)finishTransaction:(SKPaymentTransaction *)transaction { // state of transactions and finish as appropriate. - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { - for (SKPaymentTransaction *transaction in transactions) { - [self.transactionsSetter setObject:transaction forKey:transaction.transactionIdentifier]; - } + for (SKPaymentTransaction *transaction in transactions) { + [self.transactionsSetter setObject:transaction forKey:transaction.transactionIdentifier]; + } // notify dart through callbacks. if (self.transactionsUpdated) { self.transactionsUpdated(transactions); @@ -121,7 +122,7 @@ - (BOOL)paymentQueue:(SKPaymentQueue *)queue #pragma mark - getter - (NSDictionary *)transactions { - return self.transactionsSetter; + return self.transactionsSetter; } @end diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index ae3004bad132..a86a7f12e943 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -9,10 +9,10 @@ #import "FIAPaymentQueueHandler.h" typedef enum : NSUInteger { - PaymentQueueCallbackTypeUpdate, - PaymentQueueCallbackTypeRemoved, - PaymentQueueCallbackTypeRestoreTransactionFailed, - PaymentQueueCallbackTypeRestoreCompletedTransactionsFinished, + PaymentQueueCallbackTypeUpdate, + PaymentQueueCallbackTypeRemoved, + PaymentQueueCallbackTypeRestoreTransactionFailed, + PaymentQueueCallbackTypeRestoreCompletedTransactionsFinished, } PaymentQueueCallbackType; @interface InAppPurchasePlugin () @@ -25,7 +25,8 @@ @interface InAppPurchasePlugin () // for purchase. @property(copy, nonatomic) NSMutableDictionary *productsCache; // Saved payment object used for resume payments; -@property(copy, nonatomic) NSMutableDictionary *paymentsCache;; +@property(copy, nonatomic) NSMutableDictionary *paymentsCache; +; // Call back channel to dart used for when a listener function is triggered. @property(strong, nonatomic) FlutterMethodChannel *callbackChannel; @@ -50,8 +51,9 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [self canMakePayments:result]; } else if ([@"-[InAppPurchasePlugin startProductRequest:result:]" isEqualToString:call.method]) { [self handleProductRequestMethodCall:call result:result]; - } else if ([@"-[InAppPurchasePlugin createPaymentWithProductID:result:]" isEqualToString:call.method]) { - [self createPaymentWithProductID:call result:result]; + } else if ([@"-[InAppPurchasePlugin createPaymentWithProductID:result:]" + isEqualToString:call.method]) { + [self createPaymentWithProductID:call result:result]; } else if ([@"-[InAppPurchasePlugin addPayment:result:]" isEqualToString:call.method]) { [self addPayment:call result:result]; } else if ([@"-[InAppPurchasePlugin finishTransaction:result:]" isEqualToString:call.method]) { @@ -62,35 +64,35 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } - (instancetype)initWithRegistrar:(NSObject *)registrar { - self = [self init]; - self.registrar = registrar; - self.registry = [registrar textures]; - self.messenger = [registrar messenger]; - __weak typeof(self) weakSelf = self; - self.paymentQueueHandler = - [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueue defaultQueue] - transactionsUpdated:^(NSArray *_Nonnull transactions) { - [weakSelf handleTransactionsUpdated:transactions]; - } - transactionRemoved:^(NSArray *_Nonnull transactions) { - [weakSelf handleTransactionsRemoved:transactions]; - } - restoreTransactionFailed:^(NSError *_Nonnull error) { - [self handleTransactionRestoreFailed:error]; - } - restoreCompletedTransactionsFinished:^{ - [self restoreCompletedTransactionsFinished]; - } - shouldAddStorePayment:^BOOL(SKPayment *payment, SKProduct *product) { - return [self shouldAddStorePayment:payment product:product]; - } - updatedDownloads:^void(NSArray *_Nonnull downloads) { - [self updatedDownloads:downloads]; - }]; - self.callbackChannel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase_callback" - binaryMessenger:[registrar messenger]]; - return self; + self = [self init]; + self.registrar = registrar; + self.registry = [registrar textures]; + self.messenger = [registrar messenger]; + __weak typeof(self) weakSelf = self; + self.paymentQueueHandler = + [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueue defaultQueue] + transactionsUpdated:^(NSArray *_Nonnull transactions) { + [weakSelf handleTransactionsUpdated:transactions]; + } + transactionRemoved:^(NSArray *_Nonnull transactions) { + [weakSelf handleTransactionsRemoved:transactions]; + } + restoreTransactionFailed:^(NSError *_Nonnull error) { + [self handleTransactionRestoreFailed:error]; + } + restoreCompletedTransactionsFinished:^{ + [self restoreCompletedTransactionsFinished]; + } + shouldAddStorePayment:^BOOL(SKPayment *payment, SKProduct *product) { + return [self shouldAddStorePayment:payment product:product]; + } + updatedDownloads:^void(NSArray *_Nonnull downloads) { + [self updatedDownloads:downloads]; + }]; + self.callbackChannel = + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase_callback" + binaryMessenger:[registrar messenger]]; + return self; } - (void)canMakePayments:(FlutterResult)result { @@ -128,136 +130,146 @@ - (void)handleProductRequestMethodCall:(FlutterMethodCall *)call result:(Flutter details:call.arguments]); return; } - for (SKProduct *product in response.products) { + for (SKProduct *product in response.products) { [self.productsCache setObject:product forKey:product.productIdentifier]; - } + } result([FIAObjectTranslator getMapFromSKProductsResponse:response]); [weakSelf.requestHandlers removeObject:handler]; }]; } - (void)createPaymentWithProductID:(FlutterMethodCall *)call result:(FlutterResult)result { - if (![call.arguments isKindOfClass:[NSString class]]) { - result([FlutterError errorWithCode:@"storekit_invalide_argument" - message:@"Argument type of createPaymentWithProductID is not a string." - details:call.arguments]); - return; - } - NSString *productID = call.arguments; - SKProduct *product = [self.productsCache objectForKey:productID]; - if (!product) { - result([FlutterError errorWithCode:@"storekit_product_not_found" - message:@"Cannot find the product. To create a payment of a product, you must query the product with SKProductRequestMaker.startProductRequest." - details:call.arguments]); - return; - } - SKPayment *payment = [SKPayment paymentWithProduct:product]; - [self.paymentsCache setObject:payment forKey:productID]; - result([FIAObjectTranslator getMapFromSKPayment:payment]); + if (![call.arguments isKindOfClass:[NSString class]]) { + result([FlutterError + errorWithCode:@"storekit_invalide_argument" + message:@"Argument type of createPaymentWithProductID is not a string." + details:call.arguments]); + return; + } + NSString *productID = call.arguments; + SKProduct *product = [self.productsCache objectForKey:productID]; + if (!product) { + result([FlutterError + errorWithCode:@"storekit_product_not_found" + message:@"Cannot find the product. To create a payment of a product, you must query " + @"the product with SKProductRequestMaker.startProductRequest." + details:call.arguments]); + return; + } + SKPayment *payment = [SKPayment paymentWithProduct:product]; + [self.paymentsCache setObject:payment forKey:productID]; + result([FIAObjectTranslator getMapFromSKPayment:payment]); } - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { - if (![call.arguments isKindOfClass:[NSDictionary class]]) { - result([FlutterError errorWithCode:@"storekit_invalide_argument" - message:@"Argument type of addPayment is not a map" - details:call.arguments]); - return; + if (![call.arguments isKindOfClass:[NSDictionary class]]) { + result([FlutterError errorWithCode:@"storekit_invalide_argument" + message:@"Argument type of addPayment is not a map" + details:call.arguments]); + return; + } + NSDictionary *paymentMap = (NSDictionary *)call.arguments; + NSString *productID = [paymentMap objectForKey:@"productID"]; + SKPayment *payment = [self.paymentsCache objectForKey:productID]; + // User can use payment object with mutable = true and add simulatesAskToBuyInSandBox = true to + // test the payment flow. + if (!payment || [paymentMap[@"mutable"] boolValue] == YES) { + SKMutablePayment *mutablePayment = [[SKMutablePayment alloc] init]; + mutablePayment.productIdentifier = productID; + NSNumber *quantity = [paymentMap objectForKey:@"quantity"]; + if (quantity) { + mutablePayment.quantity = quantity.integerValue; } - NSDictionary *paymentMap = (NSDictionary *)call.arguments; - NSString *productID = [paymentMap objectForKey:@"productID"]; - SKPayment *payment = [self.paymentsCache objectForKey:productID]; - // User can use payment object with mutable = true and add simulatesAskToBuyInSandBox = true to test the payment flow. - if (!payment || [paymentMap[@"mutable"] boolValue] == YES) { - SKMutablePayment *mutablePayment = [[SKMutablePayment alloc] init]; - mutablePayment.productIdentifier = productID; - NSNumber *quantity = [paymentMap objectForKey:@"quantity"]; - if (quantity) { - mutablePayment.quantity = quantity.integerValue; - } - NSString *applicationUsername = [paymentMap objectForKey:@"applicationUsername"]; - mutablePayment.applicationUsername = applicationUsername; - if (@available(iOS 8.3, *)) { - mutablePayment.simulatesAskToBuyInSandbox = - [[paymentMap objectForKey:@"simulatesAskToBuyInSandBox"] boolValue]; - } - [self.paymentQueueHandler addPayment:payment]; - } else { - [self.paymentQueueHandler addPayment:payment]; + NSString *applicationUsername = [paymentMap objectForKey:@"applicationUsername"]; + mutablePayment.applicationUsername = applicationUsername; + if (@available(iOS 8.3, *)) { + mutablePayment.simulatesAskToBuyInSandbox = + [[paymentMap objectForKey:@"simulatesAskToBuyInSandBox"] boolValue]; } + [self.paymentQueueHandler addPayment:payment]; + } else { + [self.paymentQueueHandler addPayment:payment]; + } } - (void)finishTransaction:(FlutterMethodCall *)call result:(FlutterResult)result { - if (![call.arguments isKindOfClass:[NSString class]]) { - result([FlutterError errorWithCode:@"storekit_invalide_argument" - message:@"Argument type of finishTransaction is not a string." - details:call.arguments]); - return; - } - NSString *identifier = call.arguments; - SKPaymentTransaction *transaction = [self.paymentQueueHandler.transactions objectForKey:identifier]; - if (!transaction) { - result([FlutterError errorWithCode:@"storekit_platform_invalid_transaction" - message:@"Invalid transaction ID is used." - details:call.arguments]); - return; - } - @try { - // finish transaction will throw exception if the transaction type is purchasing. Notify dart about this exception. - [self.paymentQueueHandler finishTransaction:transaction]; - } @catch (NSException *e){ - result([FlutterError errorWithCode:@"storekit_finish_transaction_exception" - message:e.name - details:e.description]); - return; - } + if (![call.arguments isKindOfClass:[NSString class]]) { + result([FlutterError errorWithCode:@"storekit_invalide_argument" + message:@"Argument type of finishTransaction is not a string." + details:call.arguments]); + return; + } + NSString *identifier = call.arguments; + SKPaymentTransaction *transaction = + [self.paymentQueueHandler.transactions objectForKey:identifier]; + if (!transaction) { + result([FlutterError errorWithCode:@"storekit_platform_invalid_transaction" + message:@"Invalid transaction ID is used." + details:call.arguments]); + return; + } + @try { + // finish transaction will throw exception if the transaction type is purchasing. Notify dart + // about this exception. + [self.paymentQueueHandler finishTransaction:transaction]; + } @catch (NSException *e) { + result([FlutterError errorWithCode:@"storekit_finish_transaction_exception" + message:e.name + details:e.description]); + return; + } } #pragma mark - delegates - (void)handleTransactionsUpdated:(NSArray *)transactions { - NSMutableArray *maps = [NSMutableArray new]; - for (SKPaymentTransaction *transcation in transactions) { - [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transcation]]; - } - [self.callbackChannel invokeMethod:@"updatedTransaction" arguments:maps]; + NSMutableArray *maps = [NSMutableArray new]; + for (SKPaymentTransaction *transcation in transactions) { + [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transcation]]; + } + [self.callbackChannel invokeMethod:@"updatedTransaction" arguments:maps]; } - (void)handleTransactionsRemoved:(NSArray *)transactions { - NSMutableArray *maps = [NSMutableArray new]; - for (SKPaymentTransaction *transcation in transactions) { - [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transcation]]; - } - [self.callbackChannel invokeMethod:@"removedTransaction" arguments:maps]; + NSMutableArray *maps = [NSMutableArray new]; + for (SKPaymentTransaction *transcation in transactions) { + [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transcation]]; + } + [self.callbackChannel invokeMethod:@"removedTransaction" arguments:maps]; } - (void)handleTransactionRestoreFailed:(NSError *)error { - FlutterError *fltError = [FlutterError errorWithCode:error.domain - message:error.description - details:error.description]; - [self.callbackChannel invokeMethod:@"restoreCompletedTransactions" arguments:fltError]; + FlutterError *fltError = [FlutterError errorWithCode:error.domain + message:error.description + details:error.description]; + [self.callbackChannel invokeMethod:@"restoreCompletedTransactions" arguments:fltError]; } - (void)restoreCompletedTransactionsFinished { - [self.callbackChannel invokeMethod:@"paymentQueueRestoreCompletedTransactionsFinished" - arguments:nil]; + [self.callbackChannel invokeMethod:@"paymentQueueRestoreCompletedTransactionsFinished" + arguments:nil]; } - (void)updatedDownloads:(NSArray *)downloads { - NSMutableArray *maps = [NSMutableArray new]; - for (SKDownload *download in downloads) { - [maps addObject:[FIAObjectTranslator getMapFromSKDownload:download]]; - } - [self.callbackChannel invokeMethod:@"updatedDownloads" arguments:maps]; + NSMutableArray *maps = [NSMutableArray new]; + for (SKDownload *download in downloads) { + [maps addObject:[FIAObjectTranslator getMapFromSKDownload:download]]; + } + [self.callbackChannel invokeMethod:@"updatedDownloads" arguments:maps]; } - (BOOL)shouldAddStorePayment:(SKPayment *)payment product:(SKProduct *)product { - // We alwasy return NO here. And we send the message to dart to process the payment; and we will have a incerpection method that deciding if the payment should be processed (implemented by the programmer). - [self.productsCache setObject:product forKey:product.productIdentifier]; - [self.paymentsCache setObject:payment forKey:payment.productIdentifier]; - [self.callbackChannel invokeMethod:@"shouldAddStorePayment" arguments:@{@"payment":[FIAObjectTranslator getMapFromSKPayment:payment], - @"proudt":[FIAObjectTranslator getMapFromSKProduct:product] - }]; - return NO; + // We alwasy return NO here. And we send the message to dart to process the payment; and we will + // have a incerpection method that deciding if the payment should be processed (implemented by the + // programmer). + [self.productsCache setObject:product forKey:product.productIdentifier]; + [self.paymentsCache setObject:payment forKey:payment.productIdentifier]; + [self.callbackChannel invokeMethod:@"shouldAddStorePayment" + arguments:@{ + @"payment" : [FIAObjectTranslator getMapFromSKPayment:payment], + @"proudt" : [FIAObjectTranslator getMapFromSKProduct:product] + }]; + return NO; } #pragma mark - dependency injection (for unit testing) @@ -276,17 +288,17 @@ - (NSSet *)requestHandlers { } - (NSMutableDictionary *)productsCache { - if (!_productsCache) { - _productsCache = [NSMutableDictionary new]; - } - return _productsCache; + if (!_productsCache) { + _productsCache = [NSMutableDictionary new]; + } + return _productsCache; } - (NSMutableDictionary *)paymentsCache { - if (!_paymentsCache) { - _paymentsCache = [NSMutableDictionary new]; - } - return _paymentsCache; + if (!_paymentsCache) { + _paymentsCache = [NSMutableDictionary new]; + } + return _paymentsCache; } @end From 6878875ea9606f108efc4c61e32fbe16b62d121d Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Tue, 19 Feb 2019 12:34:25 -0800 Subject: [PATCH 10/64] draft --- .../sk_payment_queue_wrapper.dart | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index 2712998667bb..932d66a3e6ac 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -6,6 +6,8 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:in_app_purchase/src/channel.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:flutter/services.dart'; +import './sk_product_wrapper.dart'; part 'sk_payment_queue_wrapper.g.dart'; @@ -14,9 +16,129 @@ part 'sk_payment_queue_wrapper.g.dart'; /// The payment queue contains payment related operations. It communicates with App Store and presents /// a user interface for the user to process and authorize the payment. class SKPaymentQueueWrapper { + Set _observers = Set(); + + factory SKPaymentQueueWrapper() { + return _singleton; + } + + static final SKPaymentQueueWrapper _singleton = new SKPaymentQueueWrapper._(); + + SKPaymentQueueWrapper._(); + /// Calls [`-[SKPaymentQueue canMakePayments:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506139-canmakepayments?language=objc). static Future canMakePayments() async => await channel.invokeMethod('-[SKPaymentQueue canMakePayments:]'); + + /// Adds a transaction observer to listen to all the transaction events of the payment queue. + /// + /// You have to have at least one transaction observer to be added to the payment queue to handle the + /// payment follow. + void addTransactionObserver(SKTransactionObserverWrapper observer) { + _observers.add(observer); + } + + // Digests a method channel call from the platform and triggers the correct observer method. + Future _handleObserverCallbacks(MethodCall call) async { + switch (call.method) { + case 'updatedTransaction': + { + final List transactions = + _getTransactionList(call.arguments); + return await Future(() { + for (SKTransactionObserverWrapper observer in _observers) { + observer.updatedTransaction(transactions: transactions); + } + }); + } + break; + case 'removedTransaction': + { + final List transactions = + _getTransactionList(call.arguments); + return await Future(() { + for (SKTransactionObserverWrapper observer in _observers) { + observer.removedTransaction(transactions: transactions); + } + }); + } + break; + case 'paymentQueueRestoreCompletedTransactionsFinished': + { + return await Future(() { + for (SKTransactionObserverWrapper observer in _observers) { + observer.restoreCompletedTransactions(); + } + }); + } + case 'updatedDownloads': + { + final List downloads = + _getDownloadList(call.arguments); + return await Future(() { + for (SKTransactionObserverWrapper observer in _observers) { + observer.updatedDownloads(downloads: downloads); + } + }); + } + break; + case 'shouldAddStorePayment': + { + return await Future(() { + for (SKTransactionObserverWrapper observer in _observers) {} + }); + } + default: + break; + } + return null; + } + + // Get transaction wrapper object list from arguments. + List _getTransactionList(dynamic arguments) { + final List> transactionsMap = arguments; + final List transactions = transactionsMap + .map( + (Map map) => SKPaymentTransactionWrapper()) + .toList(); + return transactions; + } + + // Get download wrapper object list from arguments. + List _getDownloadList(dynamic arguments) { + final List> downloadsMap = arguments; + final List downloads = downloadsMap + .map( + (Map map) => SKDownloadWrapper()) + .toList(); + return downloads; + } +} + +/// This class is Dart wrapper around [SKTransactionObserver](https://developer.apple.com/documentation/storekit/skpaymenttransactionobserver?language=objc). +/// +/// Must be subclassed and +abstract class SKTransactionObserverWrapper { + /// Triggered when some transactions are updateded. + void updatedTransaction({List transactions}); + + /// Triggered when some transactions are removed from the payment queue. + void removedTransaction({List transactions}); + + /// Triggered when there is an error while restoring transactions. + void restoreCompletedTransactions({Error error}); + + /// Triggered when payment queue has finished sending restored transactions. + void paymentQueueRestoreCompletedTransactionsFinished(); + + /// Triggered when some download objects are updated. + void updatedDownloads({List downloads}); + + /// Triggered when a user initiated an in-app purchase from App Store. + bool shouldAddStorePayment( + {SKPaymentWrapper payment, SKProductWrapper product}) { + return true; + } } /// Dart wrapper around StoreKit's From 8feddbf45e2f986209665834d7627508b4b78ce1 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Tue, 19 Feb 2019 12:52:06 -0800 Subject: [PATCH 11/64] draft 2 --- packages/in_app_purchase/lib/src/channel.dart | 3 ++ .../sk_payment_queue_wrapper.dart | 40 +++++++++++++++---- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/packages/in_app_purchase/lib/src/channel.dart b/packages/in_app_purchase/lib/src/channel.dart index 7fc9e1a3dda5..504440ff1782 100644 --- a/packages/in_app_purchase/lib/src/channel.dart +++ b/packages/in_app_purchase/lib/src/channel.dart @@ -6,3 +6,6 @@ import 'package:flutter/services.dart'; const MethodChannel channel = MethodChannel('plugins.flutter.io/in_app_purchase'); + +const MethodChannel callbackChannel = + MethodChannel('plugins.flutter.io/in_app_purchase_callback'); \ No newline at end of file diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index 932d66a3e6ac..bb0143246376 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -18,13 +18,18 @@ part 'sk_payment_queue_wrapper.g.dart'; class SKPaymentQueueWrapper { Set _observers = Set(); + /// Returns the default payment queue. + /// + /// We do not support instantiating a custom payment queue. factory SKPaymentQueueWrapper() { return _singleton; } static final SKPaymentQueueWrapper _singleton = new SKPaymentQueueWrapper._(); - SKPaymentQueueWrapper._(); + SKPaymentQueueWrapper._() { + callbackChannel.setMethodCallHandler(_handleObserverCallbacks); + } /// Calls [`-[SKPaymentQueue canMakePayments:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506139-canmakepayments?language=objc). static Future canMakePayments() async => @@ -38,6 +43,19 @@ class SKPaymentQueueWrapper { _observers.add(observer); } + /// Adds a payment to the default payment queue. + /// + /// Prior to this call, you have to make sure At least one [SKTransactionObserverWrapper] should be added to the payment queue + /// using [addTransactionObserver]. You also have to make sure the [SKProductWrapper] of the payment has been fetched using [SKRequestMaker.startProductRequest]. + /// Each payment will generate a [SKPaymentTransactionWrapper]. After a payment is being added to the payment queue, the [SKTransactionObserverWrapper] is responsible to handle + /// the transaction that is generated by the payment. + /// The [productIdentifier] must match one of the product that is returned in [SKRequestMaker.startProductRequest]. + static Future addPayment(String productIdentifier) async => + await channel.invokeMethod( + '-[InAppPurchasePlugin addPayment:result:]', + {'productID': productIdentifier}, + ); + // Digests a method channel call from the platform and triggers the correct observer method. Future _handleObserverCallbacks(MethodCall call) async { switch (call.method) { @@ -51,7 +69,6 @@ class SKPaymentQueueWrapper { } }); } - break; case 'removedTransaction': { final List transactions = @@ -62,12 +79,20 @@ class SKPaymentQueueWrapper { } }); } - break; + case 'restoreCompletedTransactions': + { + final Error error = call.arguments; + return await Future(() { + for (SKTransactionObserverWrapper observer in _observers) { + observer.restoreCompletedTransactions(error: error); + } + }); + } case 'paymentQueueRestoreCompletedTransactionsFinished': { return await Future(() { for (SKTransactionObserverWrapper observer in _observers) { - observer.restoreCompletedTransactions(); + observer.paymentQueueRestoreCompletedTransactionsFinished(); } }); } @@ -81,7 +106,6 @@ class SKPaymentQueueWrapper { } }); } - break; case 'shouldAddStorePayment': { return await Future(() { @@ -119,10 +143,10 @@ class SKPaymentQueueWrapper { /// /// Must be subclassed and abstract class SKTransactionObserverWrapper { - /// Triggered when some transactions are updateded. + /// Triggered when any transactions are updated. void updatedTransaction({List transactions}); - /// Triggered when some transactions are removed from the payment queue. + /// Triggered when any transactions are removed from the payment queue. void removedTransaction({List transactions}); /// Triggered when there is an error while restoring transactions. @@ -131,7 +155,7 @@ abstract class SKTransactionObserverWrapper { /// Triggered when payment queue has finished sending restored transactions. void paymentQueueRestoreCompletedTransactionsFinished(); - /// Triggered when some download objects are updated. + /// Triggered when any download objects are updated. void updatedDownloads({List downloads}); /// Triggered when a user initiated an in-app purchase from App Store. From ec177e3313c52de2e3afc1c870861434e3973fde Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Tue, 19 Feb 2019 13:33:53 -0800 Subject: [PATCH 12/64] add payment --- .../ios/Classes/InAppPurchasePlugin.m | 6 +- .../sk_payment_queue_wrapper.dart | 52 ++++++++++++--- .../sk_payment_queue_wrapper_test.dart | 65 ++++++++++--------- 3 files changed, 81 insertions(+), 42 deletions(-) diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index a86a7f12e943..e09439e4ce74 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -171,9 +171,9 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { NSDictionary *paymentMap = (NSDictionary *)call.arguments; NSString *productID = [paymentMap objectForKey:@"productID"]; SKPayment *payment = [self.paymentsCache objectForKey:productID]; - // User can use payment object with mutable = true and add simulatesAskToBuyInSandBox = true to + // User can use payment object with usePaymentObject = true and add simulatesAskToBuyInSandBox = true to // test the payment flow. - if (!payment || [paymentMap[@"mutable"] boolValue] == YES) { + if (!payment || [paymentMap[@"usePaymentObject"] boolValue] == YES) { SKMutablePayment *mutablePayment = [[SKMutablePayment alloc] init]; mutablePayment.productIdentifier = productID; NSNumber *quantity = [paymentMap objectForKey:@"quantity"]; @@ -267,7 +267,7 @@ - (BOOL)shouldAddStorePayment:(SKPayment *)payment product:(SKProduct *)product [self.callbackChannel invokeMethod:@"shouldAddStorePayment" arguments:@{ @"payment" : [FIAObjectTranslator getMapFromSKPayment:payment], - @"proudt" : [FIAObjectTranslator getMapFromSKProduct:product] + @"product" : [FIAObjectTranslator getMapFromSKProduct:product] }]; return NO; } diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index bb0143246376..dbb09586892f 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -43,14 +43,29 @@ class SKPaymentQueueWrapper { _observers.add(observer); } - /// Adds a payment to the default payment queue. + /// Adds a custom payment object to the payment queue. /// + /// Most of the time you would use [addPaymentForProduct] for making a payment. This method is reserved for the necessity + /// to test the payment in the [sandbox](https://developer.apple.com/apple-pay/sandbox-testing/). + Future addPayment(SKPaymentWrapper payment) async { + Map requestMap = payment.toMap(); + requestMap['usePaymentObject'] = true; + await channel.invokeMethod( + '-[InAppPurchasePlugin addPayment:result:]', + requestMap, + ); + } + + /// Adds a payment for a product to the default payment queue. If you would like to use a custom payment object + /// to make a payment, use [addPayment] + /// + /// Use productIdentifier as the parameter will automatically create a payment object to process the payment for you. /// Prior to this call, you have to make sure At least one [SKTransactionObserverWrapper] should be added to the payment queue /// using [addTransactionObserver]. You also have to make sure the [SKProductWrapper] of the payment has been fetched using [SKRequestMaker.startProductRequest]. /// Each payment will generate a [SKPaymentTransactionWrapper]. After a payment is being added to the payment queue, the [SKTransactionObserverWrapper] is responsible to handle /// the transaction that is generated by the payment. /// The [productIdentifier] must match one of the product that is returned in [SKRequestMaker.startProductRequest]. - static Future addPayment(String productIdentifier) async => + Future addPaymentForProduct(String productIdentifier) async => await channel.invokeMethod( '-[InAppPurchasePlugin addPayment:result:]', {'productID': productIdentifier}, @@ -80,14 +95,14 @@ class SKPaymentQueueWrapper { }); } case 'restoreCompletedTransactions': - { + { final Error error = call.arguments; return await Future(() { for (SKTransactionObserverWrapper observer in _observers) { observer.restoreCompletedTransactions(error: error); } }); - } + } case 'paymentQueueRestoreCompletedTransactionsFinished': { return await Future(() { @@ -108,8 +123,15 @@ class SKPaymentQueueWrapper { } case 'shouldAddStorePayment': { + SKPaymentWrapper payment = SKPaymentWrapper.fromJson(call.arguments['payment']); + SKProductWrapper product = SKProductWrapper.fromJson(call.arguments['product']); return await Future(() { - for (SKTransactionObserverWrapper observer in _observers) {} + for (SKTransactionObserverWrapper observer in _observers) { + if (observer.shouldAddStorePayment(payment: payment, product: product)) { + SKPaymentQueueWrapper().addPaymentForProduct(product.productIdentifier); + return; + } + } }); } default: @@ -159,10 +181,13 @@ abstract class SKTransactionObserverWrapper { void updatedDownloads({List downloads}); /// Triggered when a user initiated an in-app purchase from App Store. + /// + /// Return `true` to continue the transaction in your app. If you have multiple [SKTransactionObserverWrapper]s, the transaction + /// will continue if one [SKTransactionObserverWrapper] has [shouldAddStorePayment] returning `true`. + /// Return `false` to defer or cancel the transaction. You can also continue the transaction later by calling + /// [addPaymentForProduct] with the product you get from this method. bool shouldAddStorePayment( - {SKPaymentWrapper payment, SKProductWrapper product}) { - return true; - } + {SKPaymentWrapper payment, SKProductWrapper product}); } /// Dart wrapper around StoreKit's @@ -411,6 +436,17 @@ class SKPaymentWrapper { return _$SKPaymentWrapperFromJson(map); } + /// Creates a Map object describes the payment object. + Map toMap() { + return { + 'productIdentifier': productIdentifier, + 'applicationUsername': applicationUsername, + 'requestData': requestData, + 'quantity': quantity, + 'simulatesAskToBuyInSandbox': simulatesAskToBuyInSandbox + }; + } + /// The id for the product that the payment is for. final String productIdentifier; diff --git a/packages/in_app_purchase/test/store_kit_wrappers/sk_payment_queue_wrapper_test.dart b/packages/in_app_purchase/test/store_kit_wrappers/sk_payment_queue_wrapper_test.dart index 6f68ad067b09..fc342dbbc759 100644 --- a/packages/in_app_purchase/test/store_kit_wrappers/sk_payment_queue_wrapper_test.dart +++ b/packages/in_app_purchase/test/store_kit_wrappers/sk_payment_queue_wrapper_test.dart @@ -71,42 +71,45 @@ void main() { group('Wrapper fromJson tests', () { test('Should construct correct SKPaymentWrapper from json', () { SKPaymentWrapper payment = - SKPaymentWrapper.fromJson(buildPaymentMap(dummyPayment)); + SKPaymentWrapper.fromJson(dummyPayment.toMap()); testPayment(payment, dummyPayment); }); - }); - test('Should construct correct SKError from json', () { - SKError error = SKError.fromJson(buildErrorMap(dummyError)); - testSKError(error, dummyError); - }); + test('Should construct correct SKError from json', () { + SKError error = SKError.fromJson(buildErrorMap(dummyError)); + testSKError(error, dummyError); + }); - test('Should construct correct SKDownloadWrapper from json', () { - SKDownloadWrapper download = - SKDownloadWrapper.fromJson(buildDownloadMap(dummyDownload)); - testDownload(download, dummyDownload); - }); + test('Should construct correct SKDownloadWrapper from json', () { + SKDownloadWrapper download = + SKDownloadWrapper.fromJson(buildDownloadMap(dummyDownload)); + testDownload(download, dummyDownload); + }); - test('Should construct correct SKTransactionWrapper from json', () { - SKPaymentTransactionWrapper transaction = - SKPaymentTransactionWrapper.fromJson( - buildTransactionMap(dummyTransaction)); - testTransaction(transaction, dummyTransaction); - if (transaction.originalTransaction != null) { - testTransaction(transaction.originalTransaction, - dummyTransaction.originalTransaction); - } - }); -} + test('Should construct correct SKTransactionWrapper from json', () { + SKPaymentTransactionWrapper transaction = + SKPaymentTransactionWrapper.fromJson( + buildTransactionMap(dummyTransaction)); + testTransaction(transaction, dummyTransaction); + if (transaction.originalTransaction != null) { + testTransaction(transaction.originalTransaction, + dummyTransaction.originalTransaction); + } + }); -Map buildPaymentMap(SKPaymentWrapper paymentWrapper) { - return { - 'productIdentifier': paymentWrapper.productIdentifier, - 'applicationUsername': paymentWrapper.applicationUsername, - 'requestData': paymentWrapper.requestData, - 'quantity': paymentWrapper.quantity, - 'simulatesAskToBuyInSandbox': paymentWrapper.simulatesAskToBuyInSandbox - }; + test('Should generate correct map of the payment object', () { + Map map = dummyPayment.toMap(); + expect(map['productIdentifier'], dummyPayment.productIdentifier); + expect(map['applicationUsername'], dummyPayment.applicationUsername); + + expect(map['requestData'], dummyPayment.requestData); + + expect(map['quantity'], dummyPayment.quantity); + + expect(map['simulatesAskToBuyInSandbox'], + dummyPayment.simulatesAskToBuyInSandbox); + }); + }); } Map buildErrorMap(SKError error) { @@ -140,7 +143,7 @@ Map buildTransactionMap( Map map = { 'transactionState': SKPaymentTransactionStateWrapper.values .indexOf(SKPaymentTransactionStateWrapper.purchased), - 'payment': buildPaymentMap(transaction.payment), + 'payment': transaction.payment.toMap(), 'originalTransaction': buildTransactionMap(transaction.originalTransaction), 'transactionTimeStamp': transaction.transactionTimeStamp, 'transactionIdentifier': transaction.transactionIdentifier, From ab759850117582dcabb759d39d8e4aad5f6e45a3 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Tue, 19 Feb 2019 13:34:21 -0800 Subject: [PATCH 13/64] typo fix --- packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index a86a7f12e943..4508a2f25468 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -267,7 +267,7 @@ - (BOOL)shouldAddStorePayment:(SKPayment *)payment product:(SKProduct *)product [self.callbackChannel invokeMethod:@"shouldAddStorePayment" arguments:@{ @"payment" : [FIAObjectTranslator getMapFromSKPayment:payment], - @"proudt" : [FIAObjectTranslator getMapFromSKProduct:product] + @"product" : [FIAObjectTranslator getMapFromSKProduct:product] }]; return NO; } From ed117068a097a67a4fbd938e2e38297b90f1138a Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Tue, 19 Feb 2019 13:47:43 -0800 Subject: [PATCH 14/64] more comments --- .../lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index dbb09586892f..277e3095e5a5 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -20,7 +20,7 @@ class SKPaymentQueueWrapper { /// Returns the default payment queue. /// - /// We do not support instantiating a custom payment queue. + /// We do not support instantiating a custom payment queue, hence the singleton. factory SKPaymentQueueWrapper() { return _singleton; } From 564d13bd2eead768b71f53cec7f5b594c4b466b5 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Tue, 19 Feb 2019 13:48:20 -0800 Subject: [PATCH 15/64] remove unused code --- packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index 4508a2f25468..91f32d471e0c 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -8,13 +8,6 @@ #import "FIAPRequestHandler.h" #import "FIAPaymentQueueHandler.h" -typedef enum : NSUInteger { - PaymentQueueCallbackTypeUpdate, - PaymentQueueCallbackTypeRemoved, - PaymentQueueCallbackTypeRestoreTransactionFailed, - PaymentQueueCallbackTypeRestoreCompletedTransactionsFinished, -} PaymentQueueCallbackType; - @interface InAppPurchasePlugin () // Holding strong references to FIAPRequestHandlers. Remove the handlers from the set after From b11e3c551f9fd2cf910567f15d3a00e9a8bd601a Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Tue, 19 Feb 2019 13:56:52 -0800 Subject: [PATCH 16/64] update to weakself --- .../in_app_purchase/ios/Classes/InAppPurchasePlugin.m | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index 91f32d471e0c..258c77479104 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -71,16 +71,16 @@ - (instancetype)initWithRegistrar:(NSObject *)registrar [weakSelf handleTransactionsRemoved:transactions]; } restoreTransactionFailed:^(NSError *_Nonnull error) { - [self handleTransactionRestoreFailed:error]; + [weakSelf handleTransactionRestoreFailed:error]; } restoreCompletedTransactionsFinished:^{ - [self restoreCompletedTransactionsFinished]; + [weakSelf restoreCompletedTransactionsFinished]; } shouldAddStorePayment:^BOOL(SKPayment *payment, SKProduct *product) { - return [self shouldAddStorePayment:payment product:product]; + return [weakSelf shouldAddStorePayment:payment product:product]; } updatedDownloads:^void(NSArray *_Nonnull downloads) { - [self updatedDownloads:downloads]; + [weakSelf updatedDownloads:downloads]; }]; self.callbackChannel = [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase_callback" From 7f9e5a9114ae82c71f163347e6ef9fec0fde507f Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Tue, 19 Feb 2019 15:21:33 -0800 Subject: [PATCH 17/64] review fixes --- .../InAppPurchasePluginTest.m | 2 -- .../ios/Classes/FIAPaymentQueueHandler.h | 12 +------ .../ios/Classes/FIAPaymentQueueHandler.m | 12 ------- .../ios/Classes/InAppPurchasePlugin.m | 36 +++++++++---------- 4 files changed, 19 insertions(+), 43 deletions(-) diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m index 5a061bcb3d2e..15244ce34c33 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m @@ -121,7 +121,6 @@ - (void)testAddPaymentFailure { } updatedDownloads:nil]; [queue addTransactionObserver:self.plugin.paymentQueueHandler]; - self.plugin.paymentQueueHandler.testing = YES; [self.plugin handleMethodCall:call result:^(id r){ }]; @@ -158,7 +157,6 @@ - (void)testAddPaymentSuccessWithMockQueue { } updatedDownloads:nil]; [queue addTransactionObserver:self.plugin.paymentQueueHandler]; - self.plugin.paymentQueueHandler.testing = YES; [self.plugin handleMethodCall:call result:^(id r){ }]; diff --git a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h index 9749fb57abe5..692514ab5278 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h +++ b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h @@ -7,8 +7,6 @@ NS_ASSUME_NONNULL_BEGIN -extern NSString *const TestingProductID; - typedef void (^TransactionsUpdated)(NSArray *transactions); typedef void (^TransactionsRemoved)(NSArray *transactions); typedef void (^RestoreTransactionFailed)(NSError *error); @@ -29,17 +27,9 @@ typedef void (^UpdatedDownloads)(NSArray *downloads); shouldAddStorePayment:(nullable ShouldAddStorePayment)shouldAddStorePayment updatedDownloads:(nullable UpdatedDownloads)updatedDownloads; - (void)addPayment:(SKPayment *)payment; -// Can throw exceptions, should always used in a @try block. +// Can throw exceptions if the transaction type is purchasing, should always used in a @try block. - (void)finishTransaction:(SKPaymentTransaction *)transaction; -// Enable testing. - -// Because payment object in transaction is not KVC, we cannot mock the payment object in the -// transaction. So there is no easay way to create stubs and test the handler. -// When set to true, we always this a constant TestingProductID as product ID -// when storing and accessing the completion block from self.completionMap -@property(assign, nonatomic) BOOL testing; - @end NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m index ea4be4c507eb..bf45dbd1b0ff 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m +++ b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m @@ -4,8 +4,6 @@ #import "FIAPaymentQueueHandler.h" -NSString *const TestingProductID = @"testing"; - @interface FIAPaymentQueueHandler () @property(strong, nonatomic) SKPaymentQueue *queue; @@ -46,21 +44,11 @@ - (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue } - (void)addPayment:(SKPayment *)payment { - NSString *productID = payment.productIdentifier; - if (self.testing) { - productID = TestingProductID; - } [self.queue addPayment:payment]; } - (void)finishTransaction:(SKPaymentTransaction *)transaction { - @try { [self.queue finishTransaction:transaction]; - } @catch (NSException *e) { - // finish transaction will throw exception if the transaction type is purchasing. Raise the - // exception so the InAppPurchasePlugin will get notified. - [e raise]; - } } #pragma mark - observing diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index 258c77479104..190ba65f4ec0 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -134,7 +134,7 @@ - (void)handleProductRequestMethodCall:(FlutterMethodCall *)call result:(Flutter - (void)createPaymentWithProductID:(FlutterMethodCall *)call result:(FlutterResult)result { if (![call.arguments isKindOfClass:[NSString class]]) { result([FlutterError - errorWithCode:@"storekit_invalide_argument" + errorWithCode:@"storekit_invalid_argument" message:@"Argument type of createPaymentWithProductID is not a string." details:call.arguments]); return; @@ -145,7 +145,7 @@ - (void)createPaymentWithProductID:(FlutterMethodCall *)call result:(FlutterResu result([FlutterError errorWithCode:@"storekit_product_not_found" message:@"Cannot find the product. To create a payment of a product, you must query " - @"the product with SKProductRequestMaker.startProductRequest." + @"the product with SKProductRequestMaker.startProductRequest first." details:call.arguments]); return; } @@ -156,7 +156,7 @@ - (void)createPaymentWithProductID:(FlutterMethodCall *)call result:(FlutterResu - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { if (![call.arguments isKindOfClass:[NSDictionary class]]) { - result([FlutterError errorWithCode:@"storekit_invalide_argument" + result([FlutterError errorWithCode:@"storekit_invalid_argument" message:@"Argument type of addPayment is not a map" details:call.arguments]); return; @@ -164,30 +164,29 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { NSDictionary *paymentMap = (NSDictionary *)call.arguments; NSString *productID = [paymentMap objectForKey:@"productID"]; SKPayment *payment = [self.paymentsCache objectForKey:productID]; - // User can use payment object with mutable = true and add simulatesAskToBuyInSandBox = true to + // User can use payment object with usePaymentObject = true and add simulatesAskToBuyInSandBox = true to // test the payment flow. - if (!payment || [paymentMap[@"mutable"] boolValue] == YES) { + if (!payment || [paymentMap[@"usePaymentObject"] boolValue] == YES) { SKMutablePayment *mutablePayment = [[SKMutablePayment alloc] init]; mutablePayment.productIdentifier = productID; NSNumber *quantity = [paymentMap objectForKey:@"quantity"]; - if (quantity) { - mutablePayment.quantity = quantity.integerValue; - } + mutablePayment.quantity = quantity?quantity.integerValue:1; NSString *applicationUsername = [paymentMap objectForKey:@"applicationUsername"]; mutablePayment.applicationUsername = applicationUsername; if (@available(iOS 8.3, *)) { mutablePayment.simulatesAskToBuyInSandbox = [[paymentMap objectForKey:@"simulatesAskToBuyInSandBox"] boolValue]; } - [self.paymentQueueHandler addPayment:payment]; + [self.paymentQueueHandler addPayment:mutablePayment]; } else { [self.paymentQueueHandler addPayment:payment]; } + result(nil); } - (void)finishTransaction:(FlutterMethodCall *)call result:(FlutterResult)result { if (![call.arguments isKindOfClass:[NSString class]]) { - result([FlutterError errorWithCode:@"storekit_invalide_argument" + result([FlutterError errorWithCode:@"storekit_invalid_argument" message:@"Argument type of finishTransaction is not a string." details:call.arguments]); return; @@ -211,24 +210,25 @@ - (void)finishTransaction:(FlutterMethodCall *)call result:(FlutterResult)result details:e.description]); return; } + result(nil); } #pragma mark - delegates - (void)handleTransactionsUpdated:(NSArray *)transactions { NSMutableArray *maps = [NSMutableArray new]; - for (SKPaymentTransaction *transcation in transactions) { - [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transcation]]; + for (SKPaymentTransaction *transaction in transactions) { + [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]]; } - [self.callbackChannel invokeMethod:@"updatedTransaction" arguments:maps]; + [self.callbackChannel invokeMethod:@"updatedTransactions" arguments:maps]; } - (void)handleTransactionsRemoved:(NSArray *)transactions { NSMutableArray *maps = [NSMutableArray new]; - for (SKPaymentTransaction *transcation in transactions) { - [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transcation]]; + for (SKPaymentTransaction *transaction in transactions) { + [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]]; } - [self.callbackChannel invokeMethod:@"removedTransaction" arguments:maps]; + [self.callbackChannel invokeMethod:@"removedTransactions" arguments:maps]; } - (void)handleTransactionRestoreFailed:(NSError *)error { @@ -252,8 +252,8 @@ - (void)updatedDownloads:(NSArray *)downloads { } - (BOOL)shouldAddStorePayment:(SKPayment *)payment product:(SKProduct *)product { - // We alwasy return NO here. And we send the message to dart to process the payment; and we will - // have a incerpection method that deciding if the payment should be processed (implemented by the + // We always return NO here. And we send the message to dart to process the payment; and we will + // have a interception method that deciding if the payment should be processed (implemented by the // programmer). [self.productsCache setObject:product forKey:product.productIdentifier]; [self.paymentsCache setObject:payment forKey:payment.productIdentifier]; From 0187f9a7d1ba28d258d7b57e1481c24254b4b0f4 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Tue, 19 Feb 2019 17:26:38 -0800 Subject: [PATCH 18/64] formatting --- .../in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m | 2 +- .../in_app_purchase/ios/Classes/InAppPurchasePlugin.m | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m index bf45dbd1b0ff..994509ed258d 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m +++ b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m @@ -48,7 +48,7 @@ - (void)addPayment:(SKPayment *)payment { } - (void)finishTransaction:(SKPaymentTransaction *)transaction { - [self.queue finishTransaction:transaction]; + [self.queue finishTransaction:transaction]; } #pragma mark - observing diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index 190ba65f4ec0..56c6285747a9 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -164,13 +164,13 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { NSDictionary *paymentMap = (NSDictionary *)call.arguments; NSString *productID = [paymentMap objectForKey:@"productID"]; SKPayment *payment = [self.paymentsCache objectForKey:productID]; - // User can use payment object with usePaymentObject = true and add simulatesAskToBuyInSandBox = true to - // test the payment flow. + // User can use payment object with usePaymentObject = true and add simulatesAskToBuyInSandBox = + // true to test the payment flow. if (!payment || [paymentMap[@"usePaymentObject"] boolValue] == YES) { SKMutablePayment *mutablePayment = [[SKMutablePayment alloc] init]; mutablePayment.productIdentifier = productID; NSNumber *quantity = [paymentMap objectForKey:@"quantity"]; - mutablePayment.quantity = quantity?quantity.integerValue:1; + mutablePayment.quantity = quantity ? quantity.integerValue : 1; NSString *applicationUsername = [paymentMap objectForKey:@"applicationUsername"]; mutablePayment.applicationUsername = applicationUsername; if (@available(iOS 8.3, *)) { @@ -181,7 +181,7 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { } else { [self.paymentQueueHandler addPayment:payment]; } - result(nil); + result(nil); } - (void)finishTransaction:(FlutterMethodCall *)call result:(FlutterResult)result { From 6b2d7ea9ebe86fc67e5627ece501ae23db7809e7 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Tue, 19 Feb 2019 17:31:00 -0800 Subject: [PATCH 19/64] rename callback methods --- .../lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index 277e3095e5a5..de4129d96152 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -74,7 +74,7 @@ class SKPaymentQueueWrapper { // Digests a method channel call from the platform and triggers the correct observer method. Future _handleObserverCallbacks(MethodCall call) async { switch (call.method) { - case 'updatedTransaction': + case 'updatedTransactions': { final List transactions = _getTransactionList(call.arguments); @@ -84,7 +84,7 @@ class SKPaymentQueueWrapper { } }); } - case 'removedTransaction': + case 'removedTransactions': { final List transactions = _getTransactionList(call.arguments); From e7da33a5cade5023f3c761a3eb08b66dd681fc63 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 20 Feb 2019 09:22:07 -0800 Subject: [PATCH 20/64] nit fix --- packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index 8a1898f44f56..04fe5b785467 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -164,7 +164,7 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { NSDictionary *paymentMap = (NSDictionary *)call.arguments; NSString *productID = [paymentMap objectForKey:@"productID"]; SKPayment *payment = [self.paymentsCache objectForKey:productID]; - // User can use payment object with usePaymentObject = true and add simulatesAskToBuyInSandBox = true to + // User can use payment object with usePaymentObject = true and add simulatesAskToBuyInSandBox = true to // test the payment flow. if (!payment || [paymentMap[@"usePaymentObject"] boolValue] == YES) { SKMutablePayment *mutablePayment = [[SKMutablePayment alloc] init]; From b756e7014c67b8f42d3f4ad6b1f8887726a6b3df Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 20 Feb 2019 09:41:27 -0800 Subject: [PATCH 21/64] rearrange payment object handling code --- .../ios/Classes/InAppPurchasePlugin.m | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index 56c6285747a9..0b5e90ed8cdc 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -164,9 +164,26 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { NSDictionary *paymentMap = (NSDictionary *)call.arguments; NSString *productID = [paymentMap objectForKey:@"productID"]; SKPayment *payment = [self.paymentsCache objectForKey:productID]; - // User can use payment object with usePaymentObject = true and add simulatesAskToBuyInSandBox = - // true to test the payment flow. - if (!payment || [paymentMap[@"usePaymentObject"] boolValue] == YES) { + // Use the payment object if we find a cached payment object associate with the productID. (Used + // for App Store payment flow + // https://developer.apple.com/documentation/storekit/skpaymenttransactionobserver/2877502-paymentqueue?language=objc) + if (payment) { + [self.paymentQueueHandler addPayment:payment]; + result(nil); + return; + } + // The regular payment flow: when a product is already fetched, we create a payment object with + // the product to process the payment. + SKProduct *product = [self.productsCache objectForKey:productID]; + if (product) { + payment = [SKPayment paymentWithProduct:product]; + [self.paymentQueueHandler addPayment:payment]; + result(nil); + return; + } + // User can also use payment object with usePaymentObject = true and add + // simulatesAskToBuyInSandBox = true to test the payment flow. + if ([paymentMap[@"usePaymentObject"] boolValue] == YES) { SKMutablePayment *mutablePayment = [[SKMutablePayment alloc] init]; mutablePayment.productIdentifier = productID; NSNumber *quantity = [paymentMap objectForKey:@"quantity"]; @@ -178,10 +195,20 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { [[paymentMap objectForKey:@"simulatesAskToBuyInSandBox"] boolValue]; } [self.paymentQueueHandler addPayment:mutablePayment]; - } else { - [self.paymentQueueHandler addPayment:payment]; + result(nil); + return; } - result(nil); + result([FlutterError + errorWithCode:@"storekit_invalid_payment_object" + message: + @"You have requested a payment with an invalid payment object. A valid payment " + @"object should be one of the following. 1. Payment object that is automatically " + @"handled when the user starts an in-app purchase in the App Store; and you " + @"returned true to the `shouldAddStorePayment` method or manually request a " + @"payment with the productID that is provided in the `shouldAddStorePayment` " + @"method. 2. A payment requested for a product that has been fetched. 3. A custom " + @"payment object. This is not an error for a payment failure." + details:call.arguments]); } - (void)finishTransaction:(FlutterMethodCall *)call result:(FlutterResult)result { From 4dd5b14a87d589886bcd17fd570a02c14b08f279 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 20 Feb 2019 10:00:59 -0800 Subject: [PATCH 22/64] fix unit test, remove nil check for callback blocks --- .../InAppPurchasePluginTest.m | 6 ++++-- .../ios/Classes/FIAPaymentQueueHandler.m | 15 +-------------- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m index 15244ce34c33..cf60d9554776 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m @@ -100,7 +100,8 @@ - (void)testAddPaymentFailure { arguments:@{ @"productID" : @"123", @"quantity" : @(1), - @"simulatesAskToBuyInSandBox" : @YES + @"simulatesAskToBuyInSandBox" : @YES, + @"usePaymentObject": @YES }]; SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; queue.testState = SKPaymentTransactionStateFailed; @@ -136,7 +137,8 @@ - (void)testAddPaymentSuccessWithMockQueue { arguments:@{ @"productID" : @"123", @"quantity" : @(1), - @"simulatesAskToBuyInSandBox" : @YES + @"simulatesAskToBuyInSandBox" : @YES, + @"usePaymentObject": @YES }]; SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; queue.testState = SKPaymentTransactionStatePurchased; diff --git a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m index 994509ed258d..61fd9e1208c7 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m +++ b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m @@ -60,51 +60,38 @@ - (void)paymentQueue:(SKPaymentQueue *)queue [self.transactionsSetter setObject:transaction forKey:transaction.transactionIdentifier]; } // notify dart through callbacks. - if (self.transactionsUpdated) { - self.transactionsUpdated(transactions); - } + self.transactionsUpdated(transactions); } // Sent when transactions are removed from the queue (via finishTransaction:). - (void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray *)transactions { - if (self.transactionsRemoved) { 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 { - if (self.restoreTransactionFailed) { 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 { - if (self.paymentQueueRestoreCompletedTransactionsFinished) { self.paymentQueueRestoreCompletedTransactionsFinished(); - } } // Sent when the download state has changed. - (void)paymentQueue:(SKPaymentQueue *)queue updatedDownloads:(NSArray *)downloads { - if (self.updatedDownloads) { 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 { - if (self.shouldAddStorePayment) { return (self.shouldAddStorePayment(payment, product)); - } - return YES; } #pragma mark - getter From f3cdb83b0b27684d50528de1b02f66267aea37b3 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 20 Feb 2019 11:05:39 -0800 Subject: [PATCH 23/64] formatting --- .../InAppPurchasePluginTest.m | 4 ++-- .../ios/Classes/FIAPaymentQueueHandler.m | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m index cf60d9554776..f49122a6bf9c 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m @@ -101,7 +101,7 @@ - (void)testAddPaymentFailure { @"productID" : @"123", @"quantity" : @(1), @"simulatesAskToBuyInSandBox" : @YES, - @"usePaymentObject": @YES + @"usePaymentObject" : @YES }]; SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; queue.testState = SKPaymentTransactionStateFailed; @@ -138,7 +138,7 @@ - (void)testAddPaymentSuccessWithMockQueue { @"productID" : @"123", @"quantity" : @(1), @"simulatesAskToBuyInSandBox" : @YES, - @"usePaymentObject": @YES + @"usePaymentObject" : @YES }]; SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; queue.testState = SKPaymentTransactionStatePurchased; diff --git a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m index 61fd9e1208c7..f9a550e1f121 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m +++ b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m @@ -66,32 +66,32 @@ - (void)paymentQueue:(SKPaymentQueue *)queue // Sent when transactions are removed from the queue (via finishTransaction:). - (void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray *)transactions { - self.transactionsRemoved(transactions); + 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); + 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(); + self.paymentQueueRestoreCompletedTransactionsFinished(); } // Sent when the download state has changed. - (void)paymentQueue:(SKPaymentQueue *)queue updatedDownloads:(NSArray *)downloads { - self.updatedDownloads(downloads); + 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)); + return (self.shouldAddStorePayment(payment, product)); } #pragma mark - getter From 18f1e82b32a0fa1cb13aac5372330d871c02ad56 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 20 Feb 2019 11:23:15 -0800 Subject: [PATCH 24/64] observer update --- packages/in_app_purchase/lib/src/channel.dart | 2 +- .../sk_payment_queue_wrapper.dart | 70 ++++++++++--------- 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/packages/in_app_purchase/lib/src/channel.dart b/packages/in_app_purchase/lib/src/channel.dart index 504440ff1782..b10507067ca5 100644 --- a/packages/in_app_purchase/lib/src/channel.dart +++ b/packages/in_app_purchase/lib/src/channel.dart @@ -8,4 +8,4 @@ const MethodChannel channel = MethodChannel('plugins.flutter.io/in_app_purchase'); const MethodChannel callbackChannel = - MethodChannel('plugins.flutter.io/in_app_purchase_callback'); \ No newline at end of file + MethodChannel('plugins.flutter.io/in_app_purchase_callback'); diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index de4129d96152..0f897f4c93d6 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -16,11 +16,12 @@ part 'sk_payment_queue_wrapper.g.dart'; /// The payment queue contains payment related operations. It communicates with App Store and presents /// a user interface for the user to process and authorize the payment. class SKPaymentQueueWrapper { - Set _observers = Set(); + SKTransactionObserverWrapper _observer; /// Returns the default payment queue. /// - /// We do not support instantiating a custom payment queue, hence the singleton. + /// We do not support instantiating a custom payment queue, hence the singleton. However, you can + /// override the observer factory SKPaymentQueueWrapper() { return _singleton; } @@ -35,12 +36,14 @@ class SKPaymentQueueWrapper { static Future canMakePayments() async => await channel.invokeMethod('-[SKPaymentQueue canMakePayments:]'); - /// Adds a transaction observer to listen to all the transaction events of the payment queue. + /// Sets a transaction observer to listen to all the transaction events of the payment queue. /// - /// You have to have at least one transaction observer to be added to the payment queue to handle the + /// You have to have a transaction observer to be added to the payment queue to handle the /// payment follow. - void addTransactionObserver(SKTransactionObserverWrapper observer) { - _observers.add(observer); + /// You must set the observer right when App launches to avoid missing callback when your user + /// started a purchase flow from the App Store. + void setTransactionObserver(SKTransactionObserverWrapper observer) { + _observer = observer; } /// Adds a custom payment object to the payment queue. @@ -48,6 +51,10 @@ class SKPaymentQueueWrapper { /// Most of the time you would use [addPaymentForProduct] for making a payment. This method is reserved for the necessity /// to test the payment in the [sandbox](https://developer.apple.com/apple-pay/sandbox-testing/). Future addPayment(SKPaymentWrapper payment) async { + if (_observer == null) { + throw Exception( + 'Trying to add a payment without an observer. Observer must be set using `setTransactionObserver` before adding a payment'); + } Map requestMap = payment.toMap(); requestMap['usePaymentObject'] = true; await channel.invokeMethod( @@ -65,11 +72,16 @@ class SKPaymentQueueWrapper { /// Each payment will generate a [SKPaymentTransactionWrapper]. After a payment is being added to the payment queue, the [SKTransactionObserverWrapper] is responsible to handle /// the transaction that is generated by the payment. /// The [productIdentifier] must match one of the product that is returned in [SKRequestMaker.startProductRequest]. - Future addPaymentForProduct(String productIdentifier) async => - await channel.invokeMethod( - '-[InAppPurchasePlugin addPayment:result:]', - {'productID': productIdentifier}, - ); + Future addPaymentForProduct(String productIdentifier) async { + if (_observer == null) { + throw Exception( + 'Trying to add a payment without an observer. Observer must be set using `setTransactionObserver` before adding a payment'); + } + await channel.invokeMethod( + '-[InAppPurchasePlugin addPayment:result:]', + {'productID': productIdentifier}, + ); + } // Digests a method channel call from the platform and triggers the correct observer method. Future _handleObserverCallbacks(MethodCall call) async { @@ -79,9 +91,7 @@ class SKPaymentQueueWrapper { final List transactions = _getTransactionList(call.arguments); return await Future(() { - for (SKTransactionObserverWrapper observer in _observers) { - observer.updatedTransaction(transactions: transactions); - } + _observer.updatedTransaction(transactions: transactions); }); } case 'removedTransactions': @@ -89,26 +99,20 @@ class SKPaymentQueueWrapper { final List transactions = _getTransactionList(call.arguments); return await Future(() { - for (SKTransactionObserverWrapper observer in _observers) { - observer.removedTransaction(transactions: transactions); - } + _observer.removedTransaction(transactions: transactions); }); } case 'restoreCompletedTransactions': { final Error error = call.arguments; return await Future(() { - for (SKTransactionObserverWrapper observer in _observers) { - observer.restoreCompletedTransactions(error: error); - } + _observer.restoreCompletedTransactions(error: error); }); } case 'paymentQueueRestoreCompletedTransactionsFinished': { return await Future(() { - for (SKTransactionObserverWrapper observer in _observers) { - observer.paymentQueueRestoreCompletedTransactionsFinished(); - } + _observer.paymentQueueRestoreCompletedTransactionsFinished(); }); } case 'updatedDownloads': @@ -116,21 +120,21 @@ class SKPaymentQueueWrapper { final List downloads = _getDownloadList(call.arguments); return await Future(() { - for (SKTransactionObserverWrapper observer in _observers) { - observer.updatedDownloads(downloads: downloads); - } + _observer.updatedDownloads(downloads: downloads); }); } case 'shouldAddStorePayment': { - SKPaymentWrapper payment = SKPaymentWrapper.fromJson(call.arguments['payment']); - SKProductWrapper product = SKProductWrapper.fromJson(call.arguments['product']); + SKPaymentWrapper payment = + SKPaymentWrapper.fromJson(call.arguments['payment']); + SKProductWrapper product = + SKProductWrapper.fromJson(call.arguments['product']); return await Future(() { - for (SKTransactionObserverWrapper observer in _observers) { - if (observer.shouldAddStorePayment(payment: payment, product: product)) { - SKPaymentQueueWrapper().addPaymentForProduct(product.productIdentifier); - return; - } + if (_observer.shouldAddStorePayment( + payment: payment, product: product)) { + SKPaymentQueueWrapper() + .addPaymentForProduct(product.productIdentifier); + return; } }); } From a37123eefd13f5630c3e11b8c156483f5195d2f3 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 20 Feb 2019 11:43:15 -0800 Subject: [PATCH 25/64] assert transaction observer is set --- .../sk_payment_queue_wrapper.dart | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index 0f897f4c93d6..6006e50abec7 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -51,10 +51,7 @@ class SKPaymentQueueWrapper { /// Most of the time you would use [addPaymentForProduct] for making a payment. This method is reserved for the necessity /// to test the payment in the [sandbox](https://developer.apple.com/apple-pay/sandbox-testing/). Future addPayment(SKPaymentWrapper payment) async { - if (_observer == null) { - throw Exception( - 'Trying to add a payment without an observer. Observer must be set using `setTransactionObserver` before adding a payment'); - } + assert(_observer != null, '[in_app_purchase]: Trying to add a payment without an observer. Observer must be set using `setTransactionObserver` before adding a payment `setTransactionObserver`. It is mandatory to set the observer at the moment when app launches.'); Map requestMap = payment.toMap(); requestMap['usePaymentObject'] = true; await channel.invokeMethod( @@ -73,18 +70,16 @@ class SKPaymentQueueWrapper { /// the transaction that is generated by the payment. /// The [productIdentifier] must match one of the product that is returned in [SKRequestMaker.startProductRequest]. Future addPaymentForProduct(String productIdentifier) async { - if (_observer == null) { - throw Exception( - 'Trying to add a payment without an observer. Observer must be set using `setTransactionObserver` before adding a payment'); - } + assert(_observer != null, '[in_app_purchase]: Trying to add a payment without an observer. Observer must be set using `setTransactionObserver` before adding a payment `setTransactionObserver`. It is mandatory to set the observer at the moment when app launches.'); await channel.invokeMethod( '-[InAppPurchasePlugin addPayment:result:]', {'productID': productIdentifier}, ); } - // Digests a method channel call from the platform and triggers the correct observer method. + // Triage a method channel call from the platform and triggers the correct observer method. Future _handleObserverCallbacks(MethodCall call) async { + assert(_observer != null, 'in_app_purchase]: (Fatal)The observer has not been set but we received a purchase transaction notification. Please ensure the observer has been set using `setTransactionObserver`. One of the major reasons this can happen is when user started a purchase flow from the App Store, iOS then opens your app automatically to finish the transaction. Make sure you added the transaction observer right at the app launches to handle this scenario.'); switch (call.method) { case 'updatedTransactions': { @@ -129,12 +124,11 @@ class SKPaymentQueueWrapper { SKPaymentWrapper.fromJson(call.arguments['payment']); SKProductWrapper product = SKProductWrapper.fromJson(call.arguments['product']); - return await Future(() { + return await Future(() { if (_observer.shouldAddStorePayment( - payment: payment, product: product)) { + payment: payment, product: product) == true) { SKPaymentQueueWrapper() .addPaymentForProduct(product.productIdentifier); - return; } }); } From 7d57282103add4d1b9bcfabaf58b4a10e67140dd Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 20 Feb 2019 11:53:07 -0800 Subject: [PATCH 26/64] finish transaction method --- .../store_kit_wrappers/sk_payment_queue_wrapper.dart | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index 6006e50abec7..889f192c5970 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -77,6 +77,15 @@ class SKPaymentQueueWrapper { ); } + /// Finishes a transaction, remove it from the queue. + /// + /// This method should be called from a observer callback when receiving notification from the payment queue. You should only + /// call this method after the transaction is successfully processed and the functionality purchased by the user is unlocked. + /// Itt will throw a Platform exception if the [SKPaymentTransactionWrapper.transactionState] is [SKPaymentTransactionStateWrapper.purchasing]. + Future finishTransaction(SKPaymentTransactionWrapper transaction) async { + await channel.invokeMethod('-[InAppPurchasePlugin finishTransaction:result:]', transaction.transactionIdentifier); + } + // Triage a method channel call from the platform and triggers the correct observer method. Future _handleObserverCallbacks(MethodCall call) async { assert(_observer != null, 'in_app_purchase]: (Fatal)The observer has not been set but we received a purchase transaction notification. Please ensure the observer has been set using `setTransactionObserver`. One of the major reasons this can happen is when user started a purchase flow from the App Store, iOS then opens your app automatically to finish the transaction. Make sure you added the transaction observer right at the app launches to handle this scenario.'); @@ -316,7 +325,7 @@ enum SKDownloadState { /// When the product is purchased, a List of [SKDownloadWrapper] object will be present in an [SKPaymentTransactionWrapper] object. /// To download the content, add the [SKDownloadWrapper] objects to the payment queue and wait for the content to be downloaded. /// You can also read the [contentURL] to get the URL of the downloaded content after the download completes. -/// Note that all downloaded files must be processed before the completion of the [SKPaymentTransactionWrapper]. +/// Note that all downloaded files must be processed before the completion of the [SKPaymentTransactionWrapper]([SKPaymentQueueWrapper.finishTransaction] is called). /// After the transaction is complete, any [SKDownloadWrapper] object in the transaction will not be able to be added to the payment queue /// and the [contentURL ]of the [SKDownloadWrapper] object will be invalid. @JsonSerializable() From c0eabdd1be7cd60fe342b300b1ee10e7ca0d16cc Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Thu, 21 Feb 2019 10:43:26 -0800 Subject: [PATCH 27/64] reslove conflicts --- packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index 195eb84becd4..df5bdf1dcaa6 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -202,15 +202,9 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { errorWithCode:@"storekit_invalid_payment_object" message: @"You have requested a payment with an invalid payment object. A valid payment " -<<<<<<< HEAD - @"object should be one of the following. 1. Payment object that is automatically " - @"handled when the user starts an in-app purchase in the App Store; and you " - @"returned true to the `shouldAddStorePayment` method or manually request a " -======= @"object should be one of the following: 1. Payment object that is automatically " @"handled when the user starts an in-app purchase in the App Store and you " @"returned true to the `shouldAddStorePayment` method or manually requested a " ->>>>>>> master @"payment with the productID that is provided in the `shouldAddStorePayment` " @"method. 2. A payment requested for a product that has been fetched. 3. A custom " @"payment object. This is not an error for a payment failure." From f11949e24c73d5d5d4fc02be1570902db072e8b6 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Thu, 21 Feb 2019 10:58:09 -0800 Subject: [PATCH 28/64] formatting --- .../sk_payment_queue_wrapper.dart | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index 889f192c5970..aba6d67ff52f 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -51,7 +51,8 @@ class SKPaymentQueueWrapper { /// Most of the time you would use [addPaymentForProduct] for making a payment. This method is reserved for the necessity /// to test the payment in the [sandbox](https://developer.apple.com/apple-pay/sandbox-testing/). Future addPayment(SKPaymentWrapper payment) async { - assert(_observer != null, '[in_app_purchase]: Trying to add a payment without an observer. Observer must be set using `setTransactionObserver` before adding a payment `setTransactionObserver`. It is mandatory to set the observer at the moment when app launches.'); + assert(_observer != null, + '[in_app_purchase]: Trying to add a payment without an observer. Observer must be set using `setTransactionObserver` before adding a payment `setTransactionObserver`. It is mandatory to set the observer at the moment when app launches.'); Map requestMap = payment.toMap(); requestMap['usePaymentObject'] = true; await channel.invokeMethod( @@ -70,7 +71,8 @@ class SKPaymentQueueWrapper { /// the transaction that is generated by the payment. /// The [productIdentifier] must match one of the product that is returned in [SKRequestMaker.startProductRequest]. Future addPaymentForProduct(String productIdentifier) async { - assert(_observer != null, '[in_app_purchase]: Trying to add a payment without an observer. Observer must be set using `setTransactionObserver` before adding a payment `setTransactionObserver`. It is mandatory to set the observer at the moment when app launches.'); + assert(_observer != null, + '[in_app_purchase]: Trying to add a payment without an observer. Observer must be set using `setTransactionObserver` before adding a payment `setTransactionObserver`. It is mandatory to set the observer at the moment when app launches.'); await channel.invokeMethod( '-[InAppPurchasePlugin addPayment:result:]', {'productID': productIdentifier}, @@ -82,13 +84,17 @@ class SKPaymentQueueWrapper { /// This method should be called from a observer callback when receiving notification from the payment queue. You should only /// call this method after the transaction is successfully processed and the functionality purchased by the user is unlocked. /// Itt will throw a Platform exception if the [SKPaymentTransactionWrapper.transactionState] is [SKPaymentTransactionStateWrapper.purchasing]. - Future finishTransaction(SKPaymentTransactionWrapper transaction) async { - await channel.invokeMethod('-[InAppPurchasePlugin finishTransaction:result:]', transaction.transactionIdentifier); + Future finishTransaction( + SKPaymentTransactionWrapper transaction) async { + await channel.invokeMethod( + '-[InAppPurchasePlugin finishTransaction:result:]', + transaction.transactionIdentifier); } // Triage a method channel call from the platform and triggers the correct observer method. Future _handleObserverCallbacks(MethodCall call) async { - assert(_observer != null, 'in_app_purchase]: (Fatal)The observer has not been set but we received a purchase transaction notification. Please ensure the observer has been set using `setTransactionObserver`. One of the major reasons this can happen is when user started a purchase flow from the App Store, iOS then opens your app automatically to finish the transaction. Make sure you added the transaction observer right at the app launches to handle this scenario.'); + assert(_observer != null, + 'in_app_purchase]: (Fatal)The observer has not been set but we received a purchase transaction notification. Please ensure the observer has been set using `setTransactionObserver`. One of the major reasons this can happen is when user started a purchase flow from the App Store, iOS then opens your app automatically to finish the transaction. Make sure you added the transaction observer right at the app launches to handle this scenario.'); switch (call.method) { case 'updatedTransactions': { @@ -135,7 +141,8 @@ class SKPaymentQueueWrapper { SKProductWrapper.fromJson(call.arguments['product']); return await Future(() { if (_observer.shouldAddStorePayment( - payment: payment, product: product) == true) { + payment: payment, product: product) == + true) { SKPaymentQueueWrapper() .addPaymentForProduct(product.productIdentifier); } From bd746bf0c51cce425a4b5801bacd33a6d0d6ff16 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Thu, 21 Feb 2019 11:39:07 -0800 Subject: [PATCH 29/64] fixes --- .../lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index aba6d67ff52f..53ac10594658 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -159,7 +159,7 @@ class SKPaymentQueueWrapper { final List> transactionsMap = arguments; final List transactions = transactionsMap .map( - (Map map) => SKPaymentTransactionWrapper()) + (Map map) => SKPaymentTransactionWrapper.fromJson(map)) .toList(); return transactions; } @@ -169,7 +169,7 @@ class SKPaymentQueueWrapper { final List> downloadsMap = arguments; final List downloads = downloadsMap .map( - (Map map) => SKDownloadWrapper()) + (Map map) => SKDownloadWrapper.fromJson(map)) .toList(); return downloads; } From 06f8495ec1cdc53ad5c131cc05aacc0218a9f7ec Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Thu, 21 Feb 2019 13:27:26 -0800 Subject: [PATCH 30/64] local test --- .../in_app_purchase/example/lib/main.dart | 40 +++++++++++++++++++ .../sk_product_wrapper.dart | 5 +-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/packages/in_app_purchase/example/lib/main.dart b/packages/in_app_purchase/example/lib/main.dart index 3844add7724c..d4557c15d36e 100644 --- a/packages/in_app_purchase/example/lib/main.dart +++ b/packages/in_app_purchase/example/lib/main.dart @@ -7,6 +7,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:in_app_purchase/in_app_purchase_connection.dart'; +import 'package:in_app_purchase/store_kit_wrappers.dart'; + void main() => runApp(MyApp()); class MyApp extends StatefulWidget { @@ -18,6 +20,7 @@ class _MyAppState extends State { @override void initState() { super.initState(); + SKPaymentQueueWrapper().setTransactionObserver(MyObserver()); } @override @@ -120,6 +123,15 @@ class _MyAppState extends State { productDetails.description, ), trailing: Text(productDetails.price), + onTap: () { + SKPaymentWrapper payment = SKPaymentWrapper( + productIdentifier: productDetails.id, + applicationUsername: '', + quantity: 1, + simulatesAskToBuyInSandbox: true, + requestData: null); + SKPaymentQueueWrapper().addPayment(payment); + }, )); }, ).toList(); @@ -129,3 +141,31 @@ class _MyAppState extends State { static ListTile buildListCard(ListTile innerTile) => ListTile(title: Card(child: innerTile)); } + +class MyObserver extends SKTransactionObserverWrapper { + void updatedTransaction({List transactions}) { + print('updatedTransaction'); + } + + void removedTransaction({List transactions}) { + print('removedTransaction'); + } + + void restoreCompletedTransactions({Error error}) { + print('restoreCompletedTransactions'); + } + + void paymentQueueRestoreCompletedTransactionsFinished() { + print('restore completed transactions finished'); + } + + void updatedDownloads({List downloads}) { + print('updatedDownloads'); + } + + bool shouldAddStorePayment( + {SKPaymentWrapper payment, SKProductWrapper product}) { + print('shouldAddStorePayment'); + return true; + } +} diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart index 18ea1fc694f2..7ab95ab65c11 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart @@ -73,8 +73,7 @@ class SKProductSubscriptionPeriodWrapper { /// This method should only be used with `map` values returned by [SKProductDiscountWrapper.fromJson] or [SKProductWrapper.fromJson]. /// The `map` parameter must not be null. factory SKProductSubscriptionPeriodWrapper.fromJson(Map map) { - assert(map != null && - (map['numberOfUnits'] == null || map['numberOfUnits'] > 0)); + assert(map != null, 'map must not be null' ); return _$SKProductSubscriptionPeriodWrapperFromJson(map); } @@ -173,7 +172,7 @@ class SKProductWrapper { /// This method should only be used with `map` values returned by [SkProductResponseWrapper.fromJson]. /// The `map` parameter must not be null. factory SKProductWrapper.fromJson(Map map) { - assert(map != null); + assert(map != null, 'map must not be null.'); return _$SKProductWrapperFromJson(map); } From 541c573b75c49582557f9653a8a9dc6759749671 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Thu, 21 Feb 2019 13:28:31 -0800 Subject: [PATCH 31/64] add assert message --- .../lib/src/store_kit_wrappers/sk_product_wrapper.dart | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart index 18ea1fc694f2..2dae8be75233 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart @@ -25,7 +25,7 @@ class SkProductResponseWrapper { /// This method should only be used with `map` values returned by [SKRequestMaker.startProductRequest]. /// The `map` parameter must not be null. factory SkProductResponseWrapper.fromJson(Map map) { - assert(map != null); + assert(map != null, 'map must not be null.'); return _$SkProductResponseWrapperFromJson(map); } @@ -73,8 +73,7 @@ class SKProductSubscriptionPeriodWrapper { /// This method should only be used with `map` values returned by [SKProductDiscountWrapper.fromJson] or [SKProductWrapper.fromJson]. /// The `map` parameter must not be null. factory SKProductSubscriptionPeriodWrapper.fromJson(Map map) { - assert(map != null && - (map['numberOfUnits'] == null || map['numberOfUnits'] > 0)); + assert(map != null, 'map must not be null.'); return _$SKProductSubscriptionPeriodWrapperFromJson(map); } @@ -123,7 +122,7 @@ class SKProductDiscountWrapper { /// This method should only be used with `map` values returned by [SKProductWrapper.fromJson]. /// The `map` parameter must not be null. factory SKProductDiscountWrapper.fromJson(Map map) { - assert(map != null); + assert(map != null, 'map must not be null.'); return _$SKProductDiscountWrapperFromJson(map); } @@ -173,7 +172,7 @@ class SKProductWrapper { /// This method should only be used with `map` values returned by [SkProductResponseWrapper.fromJson]. /// The `map` parameter must not be null. factory SKProductWrapper.fromJson(Map map) { - assert(map != null); + assert(map != null, 'map must not be null.'); return _$SKProductWrapperFromJson(map); } From a3fe92905c0fe55b7b157da3668b7a8b0183aef7 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Thu, 21 Feb 2019 14:06:04 -0800 Subject: [PATCH 32/64] fix price precision --- .../in_app_purchase/example/lib/main.dart | 22 +++++------ .../sku_details_wrapper.g.dart | 37 +++++++++++++++++-- .../sk_product_wrapper.dart | 8 +++- .../sk_product_wrapper.g.dart | 8 +++- 4 files changed, 58 insertions(+), 17 deletions(-) diff --git a/packages/in_app_purchase/example/lib/main.dart b/packages/in_app_purchase/example/lib/main.dart index 3844add7724c..f3ba2024348e 100644 --- a/packages/in_app_purchase/example/lib/main.dart +++ b/packages/in_app_purchase/example/lib/main.dart @@ -32,17 +32,17 @@ class _MyAppState extends State { FutureBuilder( future: _buildConnectionCheckTile(), builder: (BuildContext context, AsyncSnapshot snapshot) { - if (!snapshot.hasData) { - return Column(children: [ - buildListCard( - ListTile(title: const Text('Trying to connect...'))) - ]); - } else if (snapshot.error != null) { + if (snapshot.error != null) { return Column(children: [ buildListCard(ListTile( title: Text( 'Error connecting: ' + snapshot.error.toString()))) ]); + } else if (!snapshot.hasData) { + return Column(children: [ + buildListCard( + ListTile(title: const Text('Trying to connect...'))) + ]); } return Column( children: snapshot.data, @@ -55,14 +55,14 @@ class _MyAppState extends State { ['consumable', 'upgrade', 'subscription'].toSet()), builder: (BuildContext context, AsyncSnapshot snapshot) { - if (!snapshot.hasData) { - return Column(children: [ - buildListCard(ListTile(title: const Text('Loading...'))) - ]); - } else if (snapshot.error != null) { + if (snapshot.error != null) { return Center( child: Text('Error: ' + snapshot.error.toString()), ); + } else if (!snapshot.hasData) { + return Column(children: [ + buildListCard(ListTile(title: const Text('Loading...'))) + ]); } return Column( children: [ diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart index 631cd15c22e1..1c6ab07ce9b0 100644 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart +++ b/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart @@ -20,15 +20,46 @@ SkuDetailsWrapper _$SkuDetailsWrapperFromJson(Map json) { sku: json['sku'] as String, subscriptionPeriod: json['subscriptionPeriod'] as String, title: json['title'] as String, - type: const SkuTypeConverter().fromJson(json['type'] as String), + type: _$enumDecode(_$SkuTypeEnumMap, json['type']), isRewarded: json['isRewarded'] as bool); } +T _$enumDecode(Map enumValues, dynamic source) { + if (source == null) { + throw ArgumentError('A value must be provided. Supported values: ' + '${enumValues.values.join(', ')}'); + } + return enumValues.entries + .singleWhere((e) => e.value == source, + orElse: () => throw ArgumentError( + '`$source` is not one of the supported values: ' + '${enumValues.values.join(', ')}')) + .key; +} + +const _$SkuTypeEnumMap = { + SkuType.inapp: 'inapp', + SkuType.subs: 'subs' +}; + SkuDetailsResponseWrapper _$SkuDetailsResponseWrapperFromJson(Map json) { return SkuDetailsResponseWrapper( - responseCode: const BillingResponseConverter() - .fromJson(json['responseCode'] as int), + responseCode: + _$enumDecode(_$BillingResponseEnumMap, json['responseCode']), skuDetailsList: (json['skuDetailsList'] as List) .map((e) => SkuDetailsWrapper.fromJson(e as Map)) .toList()); } + +const _$BillingResponseEnumMap = { + BillingResponse.featureNotSupported: -2, + BillingResponse.ok: 0, + BillingResponse.userCanceled: 1, + BillingResponse.serviceUnavailable: 2, + BillingResponse.billingUnavailable: 3, + BillingResponse.itemUnavailable: 4, + BillingResponse.developerError: 5, + BillingResponse.error: 6, + BillingResponse.itemAlreadyOwned: 7, + BillingResponse.itemNotOwned: 8 +}; diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart index 2dae8be75233..b5117ba1f6d1 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart @@ -127,6 +127,7 @@ class SKProductDiscountWrapper { } /// The discounted price, in the currency that is defined in [priceLocale]. + @JsonKey(fromJson: _priceFromJson) final double price; /// Includes locale information about the price, e.g. `$` as the currency symbol for US locale. @@ -145,6 +146,8 @@ class SKProductDiscountWrapper { /// The [subscriptionPeriod] of the discount is independent of the product's [subscriptionPeriod], /// and their units and duration do not have to be matched. final SKProductSubscriptionPeriodWrapper subscriptionPeriod; + + static _priceFromJson(double price) => num.parse(price.toStringAsFixed(2)); } /// Dart wrapper around StoreKit's [SKProduct](https://developer.apple.com/documentation/storekit/skproduct?language=objc). @@ -205,6 +208,7 @@ class SKProductWrapper { final String subscriptionGroupIdentifier; /// The price of the product, in the currency that is defined in [priceLocale]. + @JsonKey(fromJson: _priceFromJson) final double price; /// Whether the AppStore has downloadable content for this product. @@ -241,6 +245,8 @@ class SKProductWrapper { price: priceLocale.currencySymbol + price.toString(), ); } + + static _priceFromJson(double price) => num.parse(price.toStringAsFixed(2)); } /// Object that indicates the locale of the price @@ -258,7 +264,7 @@ class PriceLocaleWrapper { /// This method should only be used with `map` values returned by [SKProductWrapper.fromJson] and [SKProductDiscountWrapper.fromJson]. /// The `map` parameter must not be null. factory PriceLocaleWrapper.fromJson(Map map) { - assert(map != null); + assert(map != null, 'map must not be null.'); return _$PriceLocaleWrapperFromJson(map); } diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart index 52467266d948..ff57b958f154 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart @@ -53,7 +53,9 @@ const _$SubscriptionPeriodUnitEnumMap = { SKProductDiscountWrapper _$SKProductDiscountWrapperFromJson(Map json) { return SKProductDiscountWrapper( - price: (json['price'] as num)?.toDouble(), + price: json['price'] == null + ? null + : SKProductDiscountWrapper._priceFromJson(json['price'] as double), priceLocale: json['priceLocale'] == null ? null : PriceLocaleWrapper.fromJson(json['priceLocale'] as Map), @@ -84,7 +86,9 @@ SKProductWrapper _$SKProductWrapperFromJson(Map json) { downloadContentVersion: json['downloadContentVersion'] as String, subscriptionGroupIdentifier: json['subscriptionGroupIdentifier'] as String, - price: (json['price'] as num)?.toDouble(), + price: json['price'] == null + ? null + : SKProductWrapper._priceFromJson(json['price'] as double), downloadable: json['downloadable'] as bool, downloadContentLengths: (json['downloadContentLengths'] as List) ?.map((e) => e as int) From 8762cfc1b710d4aa85c7bd157a1f773c69ac834a Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Thu, 21 Feb 2019 14:21:44 -0800 Subject: [PATCH 33/64] review fixes --- .../lib/src/store_kit_wrappers/sk_product_wrapper.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart index b5117ba1f6d1..5166459b8c31 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart @@ -25,7 +25,7 @@ class SkProductResponseWrapper { /// This method should only be used with `map` values returned by [SKRequestMaker.startProductRequest]. /// The `map` parameter must not be null. factory SkProductResponseWrapper.fromJson(Map map) { - assert(map != null, 'map must not be null.'); + assert(map != null, 'Map must not be null.'); return _$SkProductResponseWrapperFromJson(map); } @@ -73,7 +73,7 @@ class SKProductSubscriptionPeriodWrapper { /// This method should only be used with `map` values returned by [SKProductDiscountWrapper.fromJson] or [SKProductWrapper.fromJson]. /// The `map` parameter must not be null. factory SKProductSubscriptionPeriodWrapper.fromJson(Map map) { - assert(map != null, 'map must not be null.'); + assert(map != null, 'Map must not be null.'); return _$SKProductSubscriptionPeriodWrapperFromJson(map); } @@ -122,7 +122,7 @@ class SKProductDiscountWrapper { /// This method should only be used with `map` values returned by [SKProductWrapper.fromJson]. /// The `map` parameter must not be null. factory SKProductDiscountWrapper.fromJson(Map map) { - assert(map != null, 'map must not be null.'); + assert(map != null, 'Map must not be null.'); return _$SKProductDiscountWrapperFromJson(map); } @@ -175,7 +175,7 @@ class SKProductWrapper { /// This method should only be used with `map` values returned by [SkProductResponseWrapper.fromJson]. /// The `map` parameter must not be null. factory SKProductWrapper.fromJson(Map map) { - assert(map != null, 'map must not be null.'); + assert(map != null, 'Map must not be null.'); return _$SKProductWrapperFromJson(map); } @@ -264,7 +264,7 @@ class PriceLocaleWrapper { /// This method should only be used with `map` values returned by [SKProductWrapper.fromJson] and [SKProductDiscountWrapper.fromJson]. /// The `map` parameter must not be null. factory PriceLocaleWrapper.fromJson(Map map) { - assert(map != null, 'map must not be null.'); + assert(map != null, 'Map must not be null.'); return _$PriceLocaleWrapperFromJson(map); } From 736838e7d36ae7e148fb40c0859dff88a984ab72 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Thu, 21 Feb 2019 14:23:15 -0800 Subject: [PATCH 34/64] revert sku_details_wrapper.g --- .../sku_details_wrapper.g.dart | 37 ++----------------- 1 file changed, 3 insertions(+), 34 deletions(-) diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart index 1c6ab07ce9b0..631cd15c22e1 100644 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart +++ b/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart @@ -20,46 +20,15 @@ SkuDetailsWrapper _$SkuDetailsWrapperFromJson(Map json) { sku: json['sku'] as String, subscriptionPeriod: json['subscriptionPeriod'] as String, title: json['title'] as String, - type: _$enumDecode(_$SkuTypeEnumMap, json['type']), + type: const SkuTypeConverter().fromJson(json['type'] as String), isRewarded: json['isRewarded'] as bool); } -T _$enumDecode(Map enumValues, dynamic source) { - if (source == null) { - throw ArgumentError('A value must be provided. Supported values: ' - '${enumValues.values.join(', ')}'); - } - return enumValues.entries - .singleWhere((e) => e.value == source, - orElse: () => throw ArgumentError( - '`$source` is not one of the supported values: ' - '${enumValues.values.join(', ')}')) - .key; -} - -const _$SkuTypeEnumMap = { - SkuType.inapp: 'inapp', - SkuType.subs: 'subs' -}; - SkuDetailsResponseWrapper _$SkuDetailsResponseWrapperFromJson(Map json) { return SkuDetailsResponseWrapper( - responseCode: - _$enumDecode(_$BillingResponseEnumMap, json['responseCode']), + responseCode: const BillingResponseConverter() + .fromJson(json['responseCode'] as int), skuDetailsList: (json['skuDetailsList'] as List) .map((e) => SkuDetailsWrapper.fromJson(e as Map)) .toList()); } - -const _$BillingResponseEnumMap = { - BillingResponse.featureNotSupported: -2, - BillingResponse.ok: 0, - BillingResponse.userCanceled: 1, - BillingResponse.serviceUnavailable: 2, - BillingResponse.billingUnavailable: 3, - BillingResponse.itemUnavailable: 4, - BillingResponse.developerError: 5, - BillingResponse.error: 6, - BillingResponse.itemAlreadyOwned: 7, - BillingResponse.itemNotOwned: 8 -}; From 114b5ce7e3f434393bbe73d1e818d49ca327e8d2 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Thu, 21 Feb 2019 14:45:46 -0800 Subject: [PATCH 35/64] update price to string to avoid precision lost --- .../example/ios/in_app_purchase_pluginTests/Stubs.m | 6 ++++-- .../ios/in_app_purchase_pluginTests/TranslatorTest.m | 4 ++-- .../ios/Classes/FIAObjectTranslator.m | 4 ++-- .../src/store_kit_wrappers/sk_product_wrapper.dart | 12 +++--------- .../src/store_kit_wrappers/sk_product_wrapper.g.dart | 8 ++------ .../lib/src/store_kit_wrappers/sk_request_maker.dart | 1 + 6 files changed, 14 insertions(+), 21 deletions(-) diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m index 2c4378ee4081..9ed773449830 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m @@ -22,7 +22,8 @@ @implementation SKProductDiscountStub - (instancetype)initWithMap:(NSDictionary *)map { self = [super init]; if (self) { - [self setValue:map[@"price"] ?: [NSNull null] forKey:@"price"]; + [self setValue:[[NSDecimalNumber alloc] initWithString:map[@"price"]] ?: [NSNull null] + forKey:@"price"]; NSLocale *locale = NSLocale.systemLocale; [self setValue:locale ?: [NSNull null] forKey:@"priceLocale"]; [self setValue:map[@"numberOfPeriods"] ?: @(0) forKey:@"numberOfPeriods"]; @@ -45,7 +46,8 @@ - (instancetype)initWithMap:(NSDictionary *)map { [self setValue:map[@"localizedDescription"] ?: [NSNull null] forKey:@"localizedDescription"]; [self setValue:map[@"localizedTitle"] ?: [NSNull null] forKey:@"localizedTitle"]; [self setValue:map[@"downloadable"] ?: @NO forKey:@"downloadable"]; - [self setValue:map[@"price"] ?: [NSNull null] forKey:@"price"]; + [self setValue:[[NSDecimalNumber alloc] initWithString:map[@"price"]] ?: [NSNull null] + forKey:@"price"]; NSLocale *locale = NSLocale.systemLocale; [self setValue:locale ?: [NSNull null] forKey:@"priceLocale"]; [self setValue:map[@"downloadContentLengths"] ?: @(0) forKey:@"downloadContentLengths"]; diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/TranslatorTest.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/TranslatorTest.m index 130c51784532..d6babfce8cf2 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/TranslatorTest.m +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/TranslatorTest.m @@ -25,14 +25,14 @@ @implementation TranslatorTest - (void)setUp { self.periodMap = @{@"numberOfUnits" : @(0), @"unit" : @(0)}; self.discountMap = @{ - @"price" : @1.0, + @"price" : @"1", @"priceLocale" : [FIAObjectTranslator getMapFromNSLocale:NSLocale.systemLocale], @"numberOfPeriods" : @1, @"subscriptionPeriod" : self.periodMap, @"paymentMode" : @1 }; self.productMap = @{ - @"price" : @1.0, + @"price" : @"1", @"priceLocale" : [FIAObjectTranslator getMapFromNSLocale:NSLocale.systemLocale], @"productIdentifier" : @"123", @"localizedTitle" : @"title", diff --git a/packages/in_app_purchase/ios/Classes/FIAObjectTranslator.m b/packages/in_app_purchase/ios/Classes/FIAObjectTranslator.m index e2d10e015385..bd9b65edd61d 100644 --- a/packages/in_app_purchase/ios/Classes/FIAObjectTranslator.m +++ b/packages/in_app_purchase/ios/Classes/FIAObjectTranslator.m @@ -17,7 +17,7 @@ + (NSDictionary *)getMapFromSKProduct:(SKProduct *)product { @"localizedTitle" : product.localizedTitle ?: [NSNull null], @"productIdentifier" : product.productIdentifier ?: [NSNull null], @"downloadable" : @(product.downloadable), - @"price" : product.price ?: [NSNull null], + @"price" : product.price.description ?: [NSNull null], @"downloadContentLengths" : product.downloadContentLengths ?: [NSNull null], @"downloadContentVersion" : product.downloadContentVersion ?: [NSNull null] @@ -57,7 +57,7 @@ + (NSDictionary *)getMapFromSKProductDiscount:(SKProductDiscount *)discount { return nil; } NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ - @"price" : discount.price ?: [NSNull null], + @"price" : discount.price.description ?: [NSNull null], @"numberOfPeriods" : @(discount.numberOfPeriods), @"subscriptionPeriod" : [FIAObjectTranslator getMapFromSKProductSubscriptionPeriod:discount.subscriptionPeriod] diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart index 5166459b8c31..ddc35bb99c56 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart @@ -127,8 +127,7 @@ class SKProductDiscountWrapper { } /// The discounted price, in the currency that is defined in [priceLocale]. - @JsonKey(fromJson: _priceFromJson) - final double price; + final String price; /// Includes locale information about the price, e.g. `$` as the currency symbol for US locale. final PriceLocaleWrapper priceLocale; @@ -146,8 +145,6 @@ class SKProductDiscountWrapper { /// The [subscriptionPeriod] of the discount is independent of the product's [subscriptionPeriod], /// and their units and duration do not have to be matched. final SKProductSubscriptionPeriodWrapper subscriptionPeriod; - - static _priceFromJson(double price) => num.parse(price.toStringAsFixed(2)); } /// Dart wrapper around StoreKit's [SKProduct](https://developer.apple.com/documentation/storekit/skproduct?language=objc). @@ -208,8 +205,7 @@ class SKProductWrapper { final String subscriptionGroupIdentifier; /// The price of the product, in the currency that is defined in [priceLocale]. - @JsonKey(fromJson: _priceFromJson) - final double price; + final String price; /// Whether the AppStore has downloadable content for this product. /// @@ -242,11 +238,9 @@ class SKProductWrapper { id: productIdentifier, title: localizedTitle, description: localizedDescription, - price: priceLocale.currencySymbol + price.toString(), + price: priceLocale.currencySymbol + price, ); } - - static _priceFromJson(double price) => num.parse(price.toStringAsFixed(2)); } /// Object that indicates the locale of the price diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart index ff57b958f154..d6392d8cd1a4 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart @@ -53,9 +53,7 @@ const _$SubscriptionPeriodUnitEnumMap = { SKProductDiscountWrapper _$SKProductDiscountWrapperFromJson(Map json) { return SKProductDiscountWrapper( - price: json['price'] == null - ? null - : SKProductDiscountWrapper._priceFromJson(json['price'] as double), + price: json['price'] as String, priceLocale: json['priceLocale'] == null ? null : PriceLocaleWrapper.fromJson(json['priceLocale'] as Map), @@ -86,9 +84,7 @@ SKProductWrapper _$SKProductWrapperFromJson(Map json) { downloadContentVersion: json['downloadContentVersion'] as String, subscriptionGroupIdentifier: json['subscriptionGroupIdentifier'] as String, - price: json['price'] == null - ? null - : SKProductWrapper._priceFromJson(json['price'] as double), + price: json['price'] as String, downloadable: json['downloadable'] as bool, downloadContentLengths: (json['downloadContentLengths'] as List) ?.map((e) => e as int) diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_request_maker.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_request_maker.dart index 295e7277315a..a0f64ee25b0f 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_request_maker.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_request_maker.dart @@ -28,6 +28,7 @@ class SKRequestMaker { '-[InAppPurchasePlugin startProductRequest:result:]', productIdentifiers, ); + print(productResponseMap); if (productResponseMap == null) { throw PlatformException( code: 'storekit_no_response', From 343a976f99269f02d6cb18c9367bdc83e193b03c Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Thu, 21 Feb 2019 14:49:18 -0800 Subject: [PATCH 36/64] remove unnecessary print statement --- .../lib/src/store_kit_wrappers/sk_request_maker.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_request_maker.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_request_maker.dart index a0f64ee25b0f..295e7277315a 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_request_maker.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_request_maker.dart @@ -28,7 +28,6 @@ class SKRequestMaker { '-[InAppPurchasePlugin startProductRequest:result:]', productIdentifiers, ); - print(productResponseMap); if (productResponseMap == null) { throw PlatformException( code: 'storekit_no_response', From f845b61174d9f73c264449144e7517982d273d98 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Thu, 21 Feb 2019 15:52:20 -0800 Subject: [PATCH 37/64] fixes --- .../ios/Classes/FIAPaymentQueueHandler.m | 5 +- .../ios/Classes/InAppPurchasePlugin.m | 87 ++++++++++--------- .../sk_payment_queue_wrapper.dart | 6 +- 3 files changed, 55 insertions(+), 43 deletions(-) diff --git a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m index f9a550e1f121..ea76cd15a04f 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m +++ b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m @@ -39,6 +39,7 @@ - (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue self.shouldAddStorePayment = shouldAddStorePayment; self.updatedDownloads = updatedDownloads; self.transactionsSetter = [NSMutableDictionary new]; + [queue addTransactionObserver:self]; } return self; } @@ -57,7 +58,9 @@ - (void)finishTransaction:(SKPaymentTransaction *)transaction { - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { for (SKPaymentTransaction *transaction in transactions) { - [self.transactionsSetter setObject:transaction forKey:transaction.transactionIdentifier]; + if (transaction.transactionIdentifier) { + [self.transactionsSetter setObject:transaction forKey:transaction.transactionIdentifier]; + } } // notify dart through callbacks. self.transactionsUpdated(transactions); diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index df5bdf1dcaa6..7b2eb82f8108 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -35,27 +35,10 @@ + (void)registerWithRegistrar:(NSObject *)registrar { FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase" binaryMessenger:[registrar messenger]]; - InAppPurchasePlugin *instance = [[InAppPurchasePlugin alloc] init]; + InAppPurchasePlugin *instance = [[InAppPurchasePlugin alloc] initWithRegistrar:registrar]; [registrar addMethodCallDelegate:instance channel:channel]; } -- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - if ([@"-[SKPaymentQueue canMakePayments:]" isEqualToString:call.method]) { - [self canMakePayments:result]; - } else if ([@"-[InAppPurchasePlugin startProductRequest:result:]" isEqualToString:call.method]) { - [self handleProductRequestMethodCall:call result:result]; - } else if ([@"-[InAppPurchasePlugin createPaymentWithProductID:result:]" - isEqualToString:call.method]) { - [self createPaymentWithProductID:call result:result]; - } else if ([@"-[InAppPurchasePlugin addPayment:result:]" isEqualToString:call.method]) { - [self addPayment:call result:result]; - } else if ([@"-[InAppPurchasePlugin finishTransaction:result:]" isEqualToString:call.method]) { - [self finishTransaction:call result:result]; - } else { - result(FlutterMethodNotImplemented); - } -} - - (instancetype)initWithRegistrar:(NSObject *)registrar { self = [self init]; self.registrar = registrar; @@ -88,6 +71,26 @@ - (instancetype)initWithRegistrar:(NSObject *)registrar return self; } +- (void)applicationDidBecomeActive:(UIApplication *)application { +} + +- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { + if ([@"-[SKPaymentQueue canMakePayments:]" isEqualToString:call.method]) { + [self canMakePayments:result]; + } else if ([@"-[InAppPurchasePlugin startProductRequest:result:]" isEqualToString:call.method]) { + [self handleProductRequestMethodCall:call result:result]; + } else if ([@"-[InAppPurchasePlugin createPaymentWithProductID:result:]" + isEqualToString:call.method]) { + [self createPaymentWithProductID:call result:result]; + } else if ([@"-[InAppPurchasePlugin addPayment:result:]" isEqualToString:call.method]) { + [self addPayment:call result:result]; + } else if ([@"-[InAppPurchasePlugin finishTransaction:result:]" isEqualToString:call.method]) { + [self finishTransaction:call result:result]; + } else { + result(FlutterMethodNotImplemented); + } +} + - (void)canMakePayments:(FlutterResult)result { result([NSNumber numberWithBool:[SKPaymentQueue canMakePayments]]); } @@ -162,7 +165,24 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { return; } NSDictionary *paymentMap = (NSDictionary *)call.arguments; - NSString *productID = [paymentMap objectForKey:@"productID"]; + NSString *productID = [paymentMap objectForKey:@"productIdentifier"]; + // User can also use payment object with usePaymentObject = true and add + // simulatesAskToBuyInSandBox = true to test the payment flow. + if ([paymentMap[@"usePaymentObject"] boolValue] == YES) { + SKMutablePayment *mutablePayment = [[SKMutablePayment alloc] init]; + mutablePayment.productIdentifier = productID; + NSNumber *quantity = [paymentMap objectForKey:@"quantity"]; + mutablePayment.quantity = quantity ? quantity.integerValue : 1; + NSString *applicationUsername = [paymentMap objectForKey:@"applicationUsername"]; + mutablePayment.applicationUsername = applicationUsername; + if (@available(iOS 8.3, *)) { + mutablePayment.simulatesAskToBuyInSandbox = + [[paymentMap objectForKey:@"simulatesAskToBuyInSandBox"] boolValue]; + } + [self.paymentQueueHandler addPayment:mutablePayment]; + result(nil); + return; + } SKPayment *payment = [self.paymentsCache objectForKey:productID]; // Use the payment object if we find a cached payment object associate with the productID. (Used // for App Store payment flow @@ -181,23 +201,6 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { result(nil); return; } - // User can also use payment object with usePaymentObject = true and add - // simulatesAskToBuyInSandBox = true to test the payment flow. - if ([paymentMap[@"usePaymentObject"] boolValue] == YES) { - SKMutablePayment *mutablePayment = [[SKMutablePayment alloc] init]; - mutablePayment.productIdentifier = productID; - NSNumber *quantity = [paymentMap objectForKey:@"quantity"]; - mutablePayment.quantity = quantity ? quantity.integerValue : 1; - NSString *applicationUsername = [paymentMap objectForKey:@"applicationUsername"]; - mutablePayment.applicationUsername = applicationUsername; - if (@available(iOS 8.3, *)) { - mutablePayment.simulatesAskToBuyInSandbox = - [[paymentMap objectForKey:@"simulatesAskToBuyInSandBox"] boolValue]; - } - [self.paymentQueueHandler addPayment:mutablePayment]; - result(nil); - return; - } result([FlutterError errorWithCode:@"storekit_invalid_payment_object" message: @@ -222,9 +225,15 @@ - (void)finishTransaction:(FlutterMethodCall *)call result:(FlutterResult)result SKPaymentTransaction *transaction = [self.paymentQueueHandler.transactions objectForKey:identifier]; if (!transaction) { - result([FlutterError errorWithCode:@"storekit_platform_invalid_transaction" - message:@"Invalid transaction ID is used." - details:call.arguments]); + result([FlutterError + errorWithCode:@"storekit_platform_invalid_transaction" + message:[NSString + stringWithFormat:@"The transaction with transactionIdentifer:%@ does not " + @"exsit. Note that if the transactionState is " + @"purchasing, the transactionIdentifier will be " + @"nil(null). And you should not finish this transaction", + transaction.transactionIdentifier] + details:call.arguments]); return; } @try { diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index 53ac10594658..ec95fd28eeb3 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -75,7 +75,7 @@ class SKPaymentQueueWrapper { '[in_app_purchase]: Trying to add a payment without an observer. Observer must be set using `setTransactionObserver` before adding a payment `setTransactionObserver`. It is mandatory to set the observer at the moment when app launches.'); await channel.invokeMethod( '-[InAppPurchasePlugin addPayment:result:]', - {'productID': productIdentifier}, + {'productIdentifier': productIdentifier}, ); } @@ -158,8 +158,8 @@ class SKPaymentQueueWrapper { List _getTransactionList(dynamic arguments) { final List> transactionsMap = arguments; final List transactions = transactionsMap - .map( - (Map map) => SKPaymentTransactionWrapper.fromJson(map)) + .map((Map map) => + SKPaymentTransactionWrapper.fromJson(map)) .toList(); return transactions; } From 36d9d2534b0ce548af0134658029a1a365635ae7 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Thu, 21 Feb 2019 15:54:51 -0800 Subject: [PATCH 38/64] test case fixes --- .../in_app_purchase_connection/app_store_connection_test.dart | 4 ++-- .../test/store_kit_wrappers/sk_product_test.dart | 4 ++-- .../test/store_kit_wrappers/sk_request_test.dart | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/in_app_purchase/test/in_app_purchase_connection/app_store_connection_test.dart b/packages/in_app_purchase/test/in_app_purchase_connection/app_store_connection_test.dart index d107b59afe58..3d2ec3f17376 100644 --- a/packages/in_app_purchase/test/in_app_purchase_connection/app_store_connection_test.dart +++ b/packages/in_app_purchase/test/in_app_purchase_connection/app_store_connection_test.dart @@ -37,7 +37,7 @@ void main() { 'unit': 2 }; final Map discountMap = { - 'price': 1.0, + 'price': '1.0', 'priceLocale': localeMap, 'numberOfPeriods': 1, 'paymentMode': 2, @@ -50,7 +50,7 @@ void main() { 'priceLocale': localeMap, 'downloadContentVersion': 'version', 'subscriptionGroupIdentifier': 'com.group', - 'price': 1.0, + 'price': '1.0', 'downloadable': true, 'downloadContentLengths': [1, 2], 'subscriptionPeriod': subMap, diff --git a/packages/in_app_purchase/test/store_kit_wrappers/sk_product_test.dart b/packages/in_app_purchase/test/store_kit_wrappers/sk_product_test.dart index 7e49730ffe88..cc806d068b76 100644 --- a/packages/in_app_purchase/test/store_kit_wrappers/sk_product_test.dart +++ b/packages/in_app_purchase/test/store_kit_wrappers/sk_product_test.dart @@ -15,7 +15,7 @@ void main() { 'unit': 2 }; final Map discountMap = { - 'price': 1.0, + 'price': '1.0', 'priceLocale': localeMap, 'numberOfPeriods': 1, 'paymentMode': 2, @@ -28,7 +28,7 @@ void main() { 'priceLocale': localeMap, 'downloadContentVersion': 'version', 'subscriptionGroupIdentifier': 'com.group', - 'price': 1.0, + 'price': '1.0', 'downloadable': true, 'downloadContentLengths': [1, 2], 'subscriptionPeriod': subMap, diff --git a/packages/in_app_purchase/test/store_kit_wrappers/sk_request_test.dart b/packages/in_app_purchase/test/store_kit_wrappers/sk_request_test.dart index 2db357355889..7bb623860a12 100644 --- a/packages/in_app_purchase/test/store_kit_wrappers/sk_request_test.dart +++ b/packages/in_app_purchase/test/store_kit_wrappers/sk_request_test.dart @@ -19,7 +19,7 @@ void main() { 'unit': 2 }; final Map discountMap = { - 'price': 1.0, + 'price': '1.0', 'priceLocale': localeMap, 'numberOfPeriods': 1, 'paymentMode': 2, @@ -32,7 +32,7 @@ void main() { 'priceLocale': localeMap, 'downloadContentVersion': 'version', 'subscriptionGroupIdentifier': 'com.group', - 'price': 1.0, + 'price': '1.0', 'downloadable': true, 'downloadContentLengths': [1, 2], 'subscriptionPeriod': subMap, From 324d7a94068f0543e0a536066b5b61f86298d330 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Thu, 21 Feb 2019 15:56:33 -0800 Subject: [PATCH 39/64] revert main.dart --- .../in_app_purchase/example/lib/main.dart | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/packages/in_app_purchase/example/lib/main.dart b/packages/in_app_purchase/example/lib/main.dart index 54e977b32a04..f3ba2024348e 100644 --- a/packages/in_app_purchase/example/lib/main.dart +++ b/packages/in_app_purchase/example/lib/main.dart @@ -7,8 +7,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:in_app_purchase/in_app_purchase_connection.dart'; -import 'package:in_app_purchase/store_kit_wrappers.dart'; - void main() => runApp(MyApp()); class MyApp extends StatefulWidget { @@ -20,7 +18,6 @@ class _MyAppState extends State { @override void initState() { super.initState(); - SKPaymentQueueWrapper().setTransactionObserver(MyObserver()); } @override @@ -123,15 +120,6 @@ class _MyAppState extends State { productDetails.description, ), trailing: Text(productDetails.price), - onTap: () { - SKPaymentWrapper payment = SKPaymentWrapper( - productIdentifier: productDetails.id, - applicationUsername: '', - quantity: 1, - simulatesAskToBuyInSandbox: true, - requestData: null); - SKPaymentQueueWrapper().addPayment(payment); - }, )); }, ).toList(); @@ -141,31 +129,3 @@ class _MyAppState extends State { static ListTile buildListCard(ListTile innerTile) => ListTile(title: Card(child: innerTile)); } - -class MyObserver extends SKTransactionObserverWrapper { - void updatedTransaction({List transactions}) { - print('updatedTransaction'); - } - - void removedTransaction({List transactions}) { - print('removedTransaction'); - } - - void restoreCompletedTransactions({Error error}) { - print('restoreCompletedTransactions'); - } - - void paymentQueueRestoreCompletedTransactionsFinished() { - print('restore completed transactions finished'); - } - - void updatedDownloads({List downloads}) { - print('updatedDownloads'); - } - - bool shouldAddStorePayment( - {SKPaymentWrapper payment, SKProductWrapper product}) { - print('shouldAddStorePayment'); - return true; - } -} From 80fcac13ac63aa73d848561bda6baaa712103ceb Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Thu, 21 Feb 2019 16:02:48 -0800 Subject: [PATCH 40/64] remove unnecessary method --- packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index 7b2eb82f8108..d48eea373a3e 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -71,9 +71,6 @@ - (instancetype)initWithRegistrar:(NSObject *)registrar return self; } -- (void)applicationDidBecomeActive:(UIApplication *)application { -} - - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { if ([@"-[SKPaymentQueue canMakePayments:]" isEqualToString:call.method]) { [self canMakePayments:result]; From 5c8b3c5cfd54e97150f03ca3cb30066e3c7adf3b Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Thu, 21 Feb 2019 21:57:50 -0800 Subject: [PATCH 41/64] remove unnessary addpayment logic --- .../InAppPurchasePluginTest.m | 28 +----- .../ios/in_app_purchase_pluginTests/Stubs.m | 4 + .../ios/Classes/InAppPurchasePlugin.m | 89 ++++--------------- .../sk_payment_queue_wrapper.dart | 32 ++----- 4 files changed, 31 insertions(+), 122 deletions(-) diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m index f49122a6bf9c..020973a7da4d 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m @@ -70,38 +70,15 @@ - (void)testGetProductResponse { XCTAssertTrue([resultArray.firstObject[@"productIdentifier"] isEqualToString:@"123"]); } -- (void)testCreatePaymentWithProduct { - XCTestExpectation* expectation = [self expectationWithDescription:@"must return a payment"]; - FlutterMethodCall* call = [FlutterMethodCall - methodCallWithMethodName:@"-[InAppPurchasePlugin startProductRequest:result:]" - arguments:@[ @"123" ]]; - FlutterMethodCall* createPaymentCall = [FlutterMethodCall - methodCallWithMethodName:@"-[InAppPurchasePlugin createPaymentWithProductID:result:]" - arguments:@"123"]; - __block NSDictionary* result; - __weak typeof(self) weakSelf = self; - [self.plugin handleMethodCall:call - result:^(id queryResult) { - [weakSelf.plugin handleMethodCall:createPaymentCall - result:^(id _Nullable r) { - result = r; - [expectation fulfill]; - }]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertTrue([result[@"productIdentifier"] isEqualToString:@"123"]); -} - - (void)testAddPaymentFailure { XCTestExpectation* expectation = [self expectationWithDescription:@"result should return failed state"]; FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" arguments:@{ - @"productID" : @"123", + @"productIdentifier" : @"123", @"quantity" : @(1), @"simulatesAskToBuyInSandBox" : @YES, - @"usePaymentObject" : @YES }]; SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; queue.testState = SKPaymentTransactionStateFailed; @@ -135,10 +112,9 @@ - (void)testAddPaymentSuccessWithMockQueue { FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" arguments:@{ - @"productID" : @"123", + @"productIdentifier" : @"123", @"quantity" : @(1), @"simulatesAskToBuyInSandBox" : @YES, - @"usePaymentObject" : @YES }]; SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; queue.testState = SKPaymentTransactionStatePurchased; diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m index 9ed773449830..400b7020abdb 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m @@ -130,6 +130,10 @@ - (SKProductRequestStub *)getProductRequestWithIdentifiers:(NSSet *)identifiers return [[SKProductRequestStub alloc] initWithProductIdentifiers:identifiers]; } +- (SKProduct *)getProduct:(NSString *)productID { + return [SKProduct new]; +} + @end @interface SKPaymentQueueStub () diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index d48eea373a3e..57d341977cbe 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -17,9 +17,7 @@ @interface InAppPurchasePlugin () // After querying the product, the available products will be saved in the map to be used // for purchase. @property(copy, nonatomic) NSMutableDictionary *productsCache; -// Saved payment object used for resume payments; -@property(copy, nonatomic) NSMutableDictionary *paymentsCache; -; + // Call back channel to dart used for when a listener function is triggered. @property(strong, nonatomic) FlutterMethodChannel *callbackChannel; @@ -76,9 +74,6 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [self canMakePayments:result]; } else if ([@"-[InAppPurchasePlugin startProductRequest:result:]" isEqualToString:call.method]) { [self handleProductRequestMethodCall:call result:result]; - } else if ([@"-[InAppPurchasePlugin createPaymentWithProductID:result:]" - isEqualToString:call.method]) { - [self createPaymentWithProductID:call result:result]; } else if ([@"-[InAppPurchasePlugin addPayment:result:]" isEqualToString:call.method]) { [self addPayment:call result:result]; } else if ([@"-[InAppPurchasePlugin finishTransaction:result:]" isEqualToString:call.method]) { @@ -131,69 +126,27 @@ - (void)handleProductRequestMethodCall:(FlutterMethodCall *)call result:(Flutter }]; } -- (void)createPaymentWithProductID:(FlutterMethodCall *)call result:(FlutterResult)result { - if (![call.arguments isKindOfClass:[NSString class]]) { - result([FlutterError - errorWithCode:@"storekit_invalid_argument" - message:@"Argument type of createPaymentWithProductID is not a string." - details:call.arguments]); - return; - } - NSString *productID = call.arguments; - SKProduct *product = [self.productsCache objectForKey:productID]; - if (!product) { - result([FlutterError - errorWithCode:@"storekit_product_not_found" - message:@"Cannot find the product. To create a payment of a product, you must query " - @"the product with SKProductRequestMaker.startProductRequest first." - details:call.arguments]); - return; - } - SKPayment *payment = [SKPayment paymentWithProduct:product]; - [self.paymentsCache setObject:payment forKey:productID]; - result([FIAObjectTranslator getMapFromSKPayment:payment]); -} - - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { if (![call.arguments isKindOfClass:[NSDictionary class]]) { result([FlutterError errorWithCode:@"storekit_invalid_argument" - message:@"Argument type of addPayment is not a map" + message:@"Argument type of addPayment is not a Dictionary" details:call.arguments]); return; } NSDictionary *paymentMap = (NSDictionary *)call.arguments; NSString *productID = [paymentMap objectForKey:@"productIdentifier"]; - // User can also use payment object with usePaymentObject = true and add - // simulatesAskToBuyInSandBox = true to test the payment flow. - if ([paymentMap[@"usePaymentObject"] boolValue] == YES) { - SKMutablePayment *mutablePayment = [[SKMutablePayment alloc] init]; - mutablePayment.productIdentifier = productID; + // When a product is already fetched, we create a payment object with + // the product to process the payment. + SKProduct *product = [self getProduct:productID]; + if (product) { + SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product]; + payment.applicationUsername = [paymentMap objectForKey:@"applicationUsername"]; NSNumber *quantity = [paymentMap objectForKey:@"quantity"]; - mutablePayment.quantity = quantity ? quantity.integerValue : 1; - NSString *applicationUsername = [paymentMap objectForKey:@"applicationUsername"]; - mutablePayment.applicationUsername = applicationUsername; + payment.quantity = quantity ? quantity.integerValue : 1; if (@available(iOS 8.3, *)) { - mutablePayment.simulatesAskToBuyInSandbox = - [[paymentMap objectForKey:@"simulatesAskToBuyInSandBox"] boolValue]; + payment.simulatesAskToBuyInSandbox = + [[paymentMap objectForKey:@"simulatesAskToBuyInSandBox"] boolValue]; } - [self.paymentQueueHandler addPayment:mutablePayment]; - result(nil); - return; - } - SKPayment *payment = [self.paymentsCache objectForKey:productID]; - // Use the payment object if we find a cached payment object associate with the productID. (Used - // for App Store payment flow - // https://developer.apple.com/documentation/storekit/skpaymenttransactionobserver/2877502-paymentqueue?language=objc) - if (payment) { - [self.paymentQueueHandler addPayment:payment]; - result(nil); - return; - } - // The regular payment flow: when a product is already fetched, we create a payment object with - // the product to process the payment. - SKProduct *product = [self.productsCache objectForKey:productID]; - if (product) { - payment = [SKPayment paymentWithProduct:product]; [self.paymentQueueHandler addPayment:payment]; result(nil); return; @@ -201,13 +154,7 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { result([FlutterError errorWithCode:@"storekit_invalid_payment_object" message: - @"You have requested a payment with an invalid payment object. A valid payment " - @"object should be one of the following: 1. Payment object that is automatically " - @"handled when the user starts an in-app purchase in the App Store and you " - @"returned true to the `shouldAddStorePayment` method or manually requested a " - @"payment with the productID that is provided in the `shouldAddStorePayment` " - @"method. 2. A payment requested for a product that has been fetched. 3. A custom " - @"payment object. This is not an error for a payment failure." + @"You have requested a payment for an invalid product. Either the `productIdentifier` of the payment is not valid or the product has not been fetched before adding the payment to the payment queue." details:call.arguments]); } @@ -289,7 +236,6 @@ - (BOOL)shouldAddStorePayment:(SKPayment *)payment product:(SKProduct *)product // have a interception method that deciding if the payment should be processed (implemented by the // programmer). [self.productsCache setObject:product forKey:product.productIdentifier]; - [self.paymentsCache setObject:payment forKey:payment.productIdentifier]; [self.callbackChannel invokeMethod:@"shouldAddStorePayment" arguments:@{ @"payment" : [FIAObjectTranslator getMapFromSKPayment:payment], @@ -304,6 +250,10 @@ - (SKProductsRequest *)getProductRequestWithIdentifiers:(NSSet *)identifiers { return [[SKProductsRequest alloc] initWithProductIdentifiers:identifiers]; } +- (SKProduct *)getProduct:(NSString *)productID { + return [self.productsCache objectForKey:productID]; +} + #pragma mark - getter - (NSSet *)requestHandlers { @@ -320,11 +270,4 @@ - (NSMutableDictionary *)productsCache { return _productsCache; } -- (NSMutableDictionary *)paymentsCache { - if (!_paymentsCache) { - _paymentsCache = [NSMutableDictionary new]; - } - return _paymentsCache; -} - @end diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index ec95fd28eeb3..f3fe4b2068be 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -46,36 +46,21 @@ class SKPaymentQueueWrapper { _observer = observer; } - /// Adds a custom payment object to the payment queue. + /// Adds a payment object to the payment queue. /// - /// Most of the time you would use [addPaymentForProduct] for making a payment. This method is reserved for the necessity - /// to test the payment in the [sandbox](https://developer.apple.com/apple-pay/sandbox-testing/). - Future addPayment(SKPaymentWrapper payment) async { - assert(_observer != null, - '[in_app_purchase]: Trying to add a payment without an observer. Observer must be set using `setTransactionObserver` before adding a payment `setTransactionObserver`. It is mandatory to set the observer at the moment when app launches.'); - Map requestMap = payment.toMap(); - requestMap['usePaymentObject'] = true; - await channel.invokeMethod( - '-[InAppPurchasePlugin addPayment:result:]', - requestMap, - ); - } - - /// Adds a payment for a product to the default payment queue. If you would like to use a custom payment object - /// to make a payment, use [addPayment] - /// - /// Use productIdentifier as the parameter will automatically create a payment object to process the payment for you. /// Prior to this call, you have to make sure At least one [SKTransactionObserverWrapper] should be added to the payment queue /// using [addTransactionObserver]. You also have to make sure the [SKProductWrapper] of the payment has been fetched using [SKRequestMaker.startProductRequest]. /// Each payment will generate a [SKPaymentTransactionWrapper]. After a payment is being added to the payment queue, the [SKTransactionObserverWrapper] is responsible to handle /// the transaction that is generated by the payment. /// The [productIdentifier] must match one of the product that is returned in [SKRequestMaker.startProductRequest]. - Future addPaymentForProduct(String productIdentifier) async { + /// to test the payment in the [sandbox](https://developer.apple.com/apple-pay/sandbox-testing/). + Future addPayment(SKPaymentWrapper payment) async { assert(_observer != null, '[in_app_purchase]: Trying to add a payment without an observer. Observer must be set using `setTransactionObserver` before adding a payment `setTransactionObserver`. It is mandatory to set the observer at the moment when app launches.'); + Map requestMap = payment.toMap(); await channel.invokeMethod( '-[InAppPurchasePlugin addPayment:result:]', - {'productIdentifier': productIdentifier}, + requestMap, ); } @@ -144,7 +129,7 @@ class SKPaymentQueueWrapper { payment: payment, product: product) == true) { SKPaymentQueueWrapper() - .addPaymentForProduct(product.productIdentifier); + .addPayment(payment); } }); } @@ -199,7 +184,7 @@ abstract class SKTransactionObserverWrapper { /// Return `true` to continue the transaction in your app. If you have multiple [SKTransactionObserverWrapper]s, the transaction /// will continue if one [SKTransactionObserverWrapper] has [shouldAddStorePayment] returning `true`. /// Return `false` to defer or cancel the transaction. You can also continue the transaction later by calling - /// [addPaymentForProduct] with the product you get from this method. + /// [addPayment] with the product you get from this method. bool shouldAddStorePayment( {SKPaymentWrapper payment, SKProductWrapper product}); } @@ -434,7 +419,7 @@ class SKError { class SKPaymentWrapper { SKPaymentWrapper( {@required this.productIdentifier, - @required this.applicationUsername, + this.applicationUsername, this.requestData, this.quantity = 1, this.simulatesAskToBuyInSandbox = false}); @@ -467,6 +452,7 @@ class SKPaymentWrapper { /// An opaque id for the user's account. /// /// Used to help the store detect irregular activity. See https://developer.apple.com/documentation/storekit/skpayment/1506116-applicationusername?language=objc for more details. + /// For example, you can use a one-way hash of the user’s account name on your server. Don’t use the Apple ID for your developer account, the user’s Apple ID, or the user’s unhashed account name on your server. final String applicationUsername; /// Reserved for future use. From d7df3be09c832bffffeca106a533964b3049c622 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 22 Feb 2019 10:40:30 -0800 Subject: [PATCH 42/64] restore transactions --- .../InAppPurchasePluginTest.m | 30 +++++++++++++++++++ .../ios/in_app_purchase_pluginTests/Stubs.m | 4 +++ .../ios/Classes/FIAPaymentQueueHandler.h | 5 ++-- .../ios/Classes/FIAPaymentQueueHandler.m | 8 +++++ .../ios/Classes/InAppPurchasePlugin.m | 22 +++++++++++--- .../sk_payment_queue_wrapper.dart | 20 ++++++++++--- 6 files changed, 79 insertions(+), 10 deletions(-) diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m index 020973a7da4d..aff17d3137d5 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m @@ -142,4 +142,34 @@ - (void)testAddPaymentSuccessWithMockQueue { XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased); } +- (void)testRestoreTransactions { + XCTestExpectation* expectation = + [self expectationWithDescription:@"result successfully restore transactions"]; + FlutterMethodCall* call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin restoreTransactions:result:]" + arguments:nil]; + SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; + queue.testState = SKPaymentTransactionStatePurchased; + __block BOOL callbackInvoked = NO; + self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray* _Nonnull transactions) { + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:^() { + callbackInvoked = YES; + [expectation fulfill]; + } + shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { + return YES; + } + updatedDownloads:nil]; + [queue addTransactionObserver:self.plugin.paymentQueueHandler]; + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertTrue(callbackInvoked); +} + @end diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m index 400b7020abdb..02d9ea6c8385 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m @@ -154,6 +154,10 @@ - (void)addPayment:(SKPayment *)payment { [self.observer paymentQueue:self updatedTransactions:@[ transaction ]]; } +- (void)restoreCompletedTransactions { + [self.observer paymentQueueRestoreCompletedTransactionsFinished:self]; +} + @end @implementation SKPaymentTransactionStub diff --git a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h index 692514ab5278..8ebb087f06be 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h +++ b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h @@ -26,9 +26,10 @@ typedef void (^UpdatedDownloads)(NSArray *downloads); (nullable RestoreCompletedTransactionsFinished)restoreCompletedTransactionsFinished shouldAddStorePayment:(nullable ShouldAddStorePayment)shouldAddStorePayment updatedDownloads:(nullable UpdatedDownloads)updatedDownloads; -- (void)addPayment:(SKPayment *)payment; +- (void)addPayment:(nonnull SKPayment *)payment; // Can throw exceptions if the transaction type is purchasing, should always used in a @try block. -- (void)finishTransaction:(SKPaymentTransaction *)transaction; +- (void)finishTransaction:(nonnull SKPaymentTransaction *)transaction; +- (void)restoreTransactions:(nullable NSString *)applicationName; @end diff --git a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m index ea76cd15a04f..c785178cd387 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m +++ b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m @@ -52,6 +52,14 @@ - (void)finishTransaction:(SKPaymentTransaction *)transaction { [self.queue finishTransaction:transaction]; } +- (void)restoreTransactions:(nullable NSString *)applicationName { + if (applicationName) { + [self.queue restoreCompletedTransactionsWithApplicationUsername:applicationName]; + } else { + [self.queue restoreCompletedTransactions]; + } +} + #pragma mark - observing // Sent when the transaction array has changed (additions or state changes). Client should check // state of transactions and finish as appropriate. diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index 57d341977cbe..d9651ebd0aae 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -18,7 +18,6 @@ @interface InAppPurchasePlugin () // for purchase. @property(copy, nonatomic) NSMutableDictionary *productsCache; - // Call back channel to dart used for when a listener function is triggered. @property(strong, nonatomic) FlutterMethodChannel *callbackChannel; @property(strong, nonatomic) NSObject *registry; @@ -78,6 +77,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [self addPayment:call result:result]; } else if ([@"-[InAppPurchasePlugin finishTransaction:result:]" isEqualToString:call.method]) { [self finishTransaction:call result:result]; + } else if ([@"-[InAppPurchasePlugin restoreTransactions:result:]" isEqualToString:call.method]) { + [self restoreTransactions:call result:result]; } else { result(FlutterMethodNotImplemented); } @@ -145,7 +146,7 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { payment.quantity = quantity ? quantity.integerValue : 1; if (@available(iOS 8.3, *)) { payment.simulatesAskToBuyInSandbox = - [[paymentMap objectForKey:@"simulatesAskToBuyInSandBox"] boolValue]; + [[paymentMap objectForKey:@"simulatesAskToBuyInSandBox"] boolValue]; } [self.paymentQueueHandler addPayment:payment]; result(nil); @@ -153,8 +154,9 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { } result([FlutterError errorWithCode:@"storekit_invalid_payment_object" - message: - @"You have requested a payment for an invalid product. Either the `productIdentifier` of the payment is not valid or the product has not been fetched before adding the payment to the payment queue." + message:@"You have requested a payment for an invalid product. Either the " + @"`productIdentifier` of the payment is not valid or the product has not been " + @"fetched before adding the payment to the payment queue." details:call.arguments]); } @@ -193,6 +195,18 @@ - (void)finishTransaction:(FlutterMethodCall *)call result:(FlutterResult)result result(nil); } +- (void)restoreTransactions:(FlutterMethodCall *)call result:(FlutterResult)result { + if (call.arguments && ![call.arguments isKindOfClass:[NSString class]]) { + result([FlutterError + errorWithCode:@"storekit_invalid_argument" + message:@"Argument is not nil and the type of finishTransaction is not a string." + details:call.arguments]); + return; + } + [self.paymentQueueHandler restoreTransactions:call.arguments]; + result(nil); +} + #pragma mark - delegates - (void)handleTransactionsUpdated:(NSArray *)transactions { diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index f3fe4b2068be..8a928f8fa093 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -76,6 +76,18 @@ class SKPaymentQueueWrapper { transaction.transactionIdentifier); } + /// Restore transactions to maintain access to content that customers have already purchased. + /// + /// For example, when a user upgrade to a new phone, they want to keep the content they purchased in the old phone. + /// This call will invoke the [SKTransactionObserverWrapper.restoreCompletedTransactions] or [SKTransactionObserverWrapper.paymentQueueRestoreCompletedTransactionsFinished] as well as [SKTransactionObserverWrapper.updatedTransaction] + /// in the [SKTransactionObserverWrapper]. If you keep the download content in your own server, in the observer methods, you can simply finish the transaction by calling [finishTransaction] and + /// download the content from your own server. + /// If you keep the download content on Apple's server, you can access the download content in the transaction object that you get from [SKTransactionObserverWrapper.updatedTransaction] when the [SKPaymentTransactionWrapper.transactionState] is [SKPaymentTransactionStateWrapper.restored]. + Future restoreTransactions({String applicationName}) async { + await channel.invokeMethod( + '-[InAppPurchasePlugin restoreTransactions:result:]', applicationName); + } + // Triage a method channel call from the platform and triggers the correct observer method. Future _handleObserverCallbacks(MethodCall call) async { assert(_observer != null, @@ -128,8 +140,7 @@ class SKPaymentQueueWrapper { if (_observer.shouldAddStorePayment( payment: payment, product: product) == true) { - SKPaymentQueueWrapper() - .addPayment(payment); + SKPaymentQueueWrapper().addPayment(payment); } }); } @@ -183,8 +194,9 @@ abstract class SKTransactionObserverWrapper { /// /// Return `true` to continue the transaction in your app. If you have multiple [SKTransactionObserverWrapper]s, the transaction /// will continue if one [SKTransactionObserverWrapper] has [shouldAddStorePayment] returning `true`. - /// Return `false` to defer or cancel the transaction. You can also continue the transaction later by calling - /// [addPayment] with the product you get from this method. + /// Return `false` to defer or cancel the transaction. For example, you may need to defer a transaction if the user is in the middle of onboarding. + /// You can also continue the transaction later by calling + /// [addPayment] with the [SKPaymentWrapper] object you get from this method. bool shouldAddStorePayment( {SKPaymentWrapper payment, SKProductWrapper product}); } From 8132c224c777b589d1bd1d53a1c9f92574ab7615 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 22 Feb 2019 11:25:21 -0800 Subject: [PATCH 43/64] formatting and renaming --- .../in_app_purchase/ios/Classes/InAppPurchasePlugin.m | 8 ++++---- .../store_kit_wrappers/sk_payment_queue_wrapper.dart | 11 +++++------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index 57d341977cbe..a22403402092 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -18,7 +18,6 @@ @interface InAppPurchasePlugin () // for purchase. @property(copy, nonatomic) NSMutableDictionary *productsCache; - // Call back channel to dart used for when a listener function is triggered. @property(strong, nonatomic) FlutterMethodChannel *callbackChannel; @property(strong, nonatomic) NSObject *registry; @@ -145,7 +144,7 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { payment.quantity = quantity ? quantity.integerValue : 1; if (@available(iOS 8.3, *)) { payment.simulatesAskToBuyInSandbox = - [[paymentMap objectForKey:@"simulatesAskToBuyInSandBox"] boolValue]; + [[paymentMap objectForKey:@"simulatesAskToBuyInSandBox"] boolValue]; } [self.paymentQueueHandler addPayment:payment]; result(nil); @@ -153,8 +152,9 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { } result([FlutterError errorWithCode:@"storekit_invalid_payment_object" - message: - @"You have requested a payment for an invalid product. Either the `productIdentifier` of the payment is not valid or the product has not been fetched before adding the payment to the payment queue." + message:@"You have requested a payment for an invalid product. Either the " + @"`productIdentifier` of the payment is not valid or the product has not been " + @"fetched before adding the payment to the payment queue." details:call.arguments]); } diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index f3fe4b2068be..3af3ef4b18b6 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -86,7 +86,7 @@ class SKPaymentQueueWrapper { final List transactions = _getTransactionList(call.arguments); return await Future(() { - _observer.updatedTransaction(transactions: transactions); + _observer.updatedTransactions(transactions: transactions); }); } case 'removedTransactions': @@ -94,7 +94,7 @@ class SKPaymentQueueWrapper { final List transactions = _getTransactionList(call.arguments); return await Future(() { - _observer.removedTransaction(transactions: transactions); + _observer.removedTransactions(transactions: transactions); }); } case 'restoreCompletedTransactions': @@ -128,8 +128,7 @@ class SKPaymentQueueWrapper { if (_observer.shouldAddStorePayment( payment: payment, product: product) == true) { - SKPaymentQueueWrapper() - .addPayment(payment); + SKPaymentQueueWrapper().addPayment(payment); } }); } @@ -165,10 +164,10 @@ class SKPaymentQueueWrapper { /// Must be subclassed and abstract class SKTransactionObserverWrapper { /// Triggered when any transactions are updated. - void updatedTransaction({List transactions}); + void updatedTransactions({List transactions}); /// Triggered when any transactions are removed from the payment queue. - void removedTransaction({List transactions}); + void removedTransactions({List transactions}); /// Triggered when there is an error while restoring transactions. void restoreCompletedTransactions({Error error}); From de3a84b3b9a9c4c28a7a2b177fb66b50433e9cfe Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 22 Feb 2019 13:11:10 -0800 Subject: [PATCH 44/64] store notifications if observer is not set --- .../InAppPurchasePluginTest.m | 70 +++++++++++++++++++ .../ios/Classes/FIAPaymentQueueHandler.h | 1 + .../ios/Classes/FIAPaymentQueueHandler.m | 35 ++++++++++ .../ios/Classes/InAppPurchasePlugin.m | 7 ++ .../sk_payment_queue_wrapper.dart | 27 +++---- 5 files changed, 127 insertions(+), 13 deletions(-) diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m index 020973a7da4d..9d01f1f9ef46 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m @@ -99,6 +99,8 @@ - (void)testAddPaymentFailure { } updatedDownloads:nil]; [queue addTransactionObserver:self.plugin.paymentQueueHandler]; + self.plugin.paymentQueueHandler.observerDidSet = YES; + [self.plugin handleMethodCall:call result:^(id r){ }]; @@ -134,6 +136,7 @@ - (void)testAddPaymentSuccessWithMockQueue { return YES; } updatedDownloads:nil]; + self.plugin.paymentQueueHandler.observerDidSet = YES; [queue addTransactionObserver:self.plugin.paymentQueueHandler]; [self.plugin handleMethodCall:call result:^(id r){ @@ -142,4 +145,71 @@ - (void)testAddPaymentSuccessWithMockQueue { XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased); } +- (void)testSetObserver { + FlutterMethodCall* call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin observerDidSet:]" + arguments:nil]; + SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; + queue.testState = SKPaymentTransactionStatePurchased; + self.plugin.paymentQueueHandler = + [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:nil + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, + SKProduct* _Nonnull product) { + return YES; + } + updatedDownloads:nil]; + [queue addTransactionObserver:self.plugin.paymentQueueHandler]; + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + [self waitForExpectations:@[ exceptToBeTrue ] timeout:5]; + XCTAssertTrue(self.plugin.paymentQueueHandler.observerDidSet); +} + +- (void)testSetObserverFlushesStoredTransactionAndPaymentAndProducts { + XCTestExpectation* expectation = + [self expectationWithDescription:@"result should return success state"]; + FlutterMethodCall* call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:@{ + @"productIdentifier" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandBox" : @YES, + }]; + SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; + queue.testState = SKPaymentTransactionStatePurchased; + __block SKPaymentTransaction* transactionForUpdateBlock; + self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray* _Nonnull transactions) { + SKPaymentTransaction* transaction = transactions[0]; + if (transaction.transactionState == SKPaymentTransactionStatePurchased) { + transactionForUpdateBlock = transaction; + [expectation fulfill]; + } + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { + return YES; + } + updatedDownloads:nil]; + [queue addTransactionObserver:self.plugin.paymentQueueHandler]; + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + FlutterMethodCall* setObserverCall = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin observerDidSet:]" + arguments:nil]; + [self.plugin handleMethodCall:setObserverCall + result:^(id r){ + }]; + [self waitForExpectations:@[ expectation, exceptToBeTrue ] timeout:5]; + XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased); +} + @end diff --git a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h index 692514ab5278..72e6dccc43ca 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h +++ b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h @@ -17,6 +17,7 @@ typedef void (^UpdatedDownloads)(NSArray *downloads); @interface FIAPaymentQueueHandler : NSObject @property(copy, nonatomic, readonly) NSDictionary *transactions; +@property(assign, nonatomic) BOOL observerDidSet; - (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated diff --git a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m index ea76cd15a04f..ea7369f0d0a4 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m +++ b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m @@ -17,6 +17,13 @@ @interface FIAPaymentQueueHandler () @property(strong, nonatomic) NSMutableDictionary *transactionsSetter; +// Store in cache and flush to the dart later after we are sure the observer is set in dart. +// This should only handle the case when app launches and there are unfinished transactions or +// the case when user initiated the payment flow from App Store. +@property(copy, nonatomic) NSArray *transactionsToUpdate; +@property(strong, nonatomic) SKPayment *paymentShouldBeAdded; +@property(strong, nonatomic) SKProduct *productShouldBePaied; + @end @implementation FIAPaymentQueueHandler @@ -52,11 +59,34 @@ - (void)finishTransaction:(SKPaymentTransaction *)transaction { [self.queue finishTransaction:transaction]; } +- (void)setObserverDidSet:(BOOL)observerDidSet { + _observerDidSet = observerDidSet; + if (observerDidSet) { + if (self.transactionsUpdated) { + [self paymentQueue:self.queue updatedTransactions:self.transactionsToUpdate]; + self.transactionsToUpdate = nil; + } + if (@available(iOS 11.0, *)) { + if (self.paymentShouldBeAdded && self.productShouldBePaied) { + [self paymentQueue:self.queue + shouldAddStorePayment:self.paymentShouldBeAdded + forProduct:self.productShouldBePaied]; + self.paymentShouldBeAdded = nil; + self.productShouldBePaied = nil; + } + } + } +} + #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 *)transactions { + if (!self.observerDidSet) { + self.transactionsToUpdate = transactions; + return; + } for (SKPaymentTransaction *transaction in transactions) { if (transaction.transactionIdentifier) { [self.transactionsSetter setObject:transaction forKey:transaction.transactionIdentifier]; @@ -94,6 +124,11 @@ - (void)paymentQueue:(SKPaymentQueue *)queue updatedDownloads:(NSArray *)transactions { diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index 3af3ef4b18b6..4459703f103b 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -42,8 +42,10 @@ class SKPaymentQueueWrapper { /// payment follow. /// You must set the observer right when App launches to avoid missing callback when your user /// started a purchase flow from the App Store. + /// Returns true is void setTransactionObserver(SKTransactionObserverWrapper observer) { _observer = observer; + channel.invokeMethod('-[SKPaymentQueue observerDidSet:]'); } /// Adds a payment object to the payment queue. @@ -54,11 +56,11 @@ class SKPaymentQueueWrapper { /// the transaction that is generated by the payment. /// The [productIdentifier] must match one of the product that is returned in [SKRequestMaker.startProductRequest]. /// to test the payment in the [sandbox](https://developer.apple.com/apple-pay/sandbox-testing/). - Future addPayment(SKPaymentWrapper payment) async { + Future addPayment(SKPaymentWrapper payment) { assert(_observer != null, - '[in_app_purchase]: Trying to add a payment without an observer. Observer must be set using `setTransactionObserver` before adding a payment `setTransactionObserver`. It is mandatory to set the observer at the moment when app launches.'); + '[in_app_purchase]: Trying to add a payment without an observer. Observer must be set using `setTransactionObserver` right at the app launch.'); Map requestMap = payment.toMap(); - await channel.invokeMethod( + return channel.invokeMethod( '-[InAppPurchasePlugin addPayment:result:]', requestMap, ); @@ -69,15 +71,14 @@ class SKPaymentQueueWrapper { /// This method should be called from a observer callback when receiving notification from the payment queue. You should only /// call this method after the transaction is successfully processed and the functionality purchased by the user is unlocked. /// Itt will throw a Platform exception if the [SKPaymentTransactionWrapper.transactionState] is [SKPaymentTransactionStateWrapper.purchasing]. - Future finishTransaction( - SKPaymentTransactionWrapper transaction) async { - await channel.invokeMethod( + Future finishTransaction(SKPaymentTransactionWrapper transaction) { + return channel.invokeMethod( '-[InAppPurchasePlugin finishTransaction:result:]', transaction.transactionIdentifier); } // Triage a method channel call from the platform and triggers the correct observer method. - Future _handleObserverCallbacks(MethodCall call) async { + Future _handleObserverCallbacks(MethodCall call) { assert(_observer != null, 'in_app_purchase]: (Fatal)The observer has not been set but we received a purchase transaction notification. Please ensure the observer has been set using `setTransactionObserver`. One of the major reasons this can happen is when user started a purchase flow from the App Store, iOS then opens your app automatically to finish the transaction. Make sure you added the transaction observer right at the app launches to handle this scenario.'); switch (call.method) { @@ -85,7 +86,7 @@ class SKPaymentQueueWrapper { { final List transactions = _getTransactionList(call.arguments); - return await Future(() { + return Future(() { _observer.updatedTransactions(transactions: transactions); }); } @@ -93,20 +94,20 @@ class SKPaymentQueueWrapper { { final List transactions = _getTransactionList(call.arguments); - return await Future(() { + return Future(() { _observer.removedTransactions(transactions: transactions); }); } case 'restoreCompletedTransactions': { final Error error = call.arguments; - return await Future(() { + return Future(() { _observer.restoreCompletedTransactions(error: error); }); } case 'paymentQueueRestoreCompletedTransactionsFinished': { - return await Future(() { + return Future(() { _observer.paymentQueueRestoreCompletedTransactionsFinished(); }); } @@ -114,7 +115,7 @@ class SKPaymentQueueWrapper { { final List downloads = _getDownloadList(call.arguments); - return await Future(() { + return Future(() { _observer.updatedDownloads(downloads: downloads); }); } @@ -124,7 +125,7 @@ class SKPaymentQueueWrapper { SKPaymentWrapper.fromJson(call.arguments['payment']); SKProductWrapper product = SKProductWrapper.fromJson(call.arguments['product']); - return await Future(() { + return Future(() { if (_observer.shouldAddStorePayment( payment: payment, product: product) == true) { From ffaf35cb274fe51d46ce18452449c3e397f080a4 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 22 Feb 2019 14:34:00 -0800 Subject: [PATCH 45/64] fix transform map to transactions and downloads --- .../sk_payment_queue_wrapper.dart | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index 3af3ef4b18b6..522eb01fff8d 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -77,15 +77,15 @@ class SKPaymentQueueWrapper { } // Triage a method channel call from the platform and triggers the correct observer method. - Future _handleObserverCallbacks(MethodCall call) async { + Future _handleObserverCallbacks(MethodCall call) { assert(_observer != null, - 'in_app_purchase]: (Fatal)The observer has not been set but we received a purchase transaction notification. Please ensure the observer has been set using `setTransactionObserver`. One of the major reasons this can happen is when user started a purchase flow from the App Store, iOS then opens your app automatically to finish the transaction. Make sure you added the transaction observer right at the app launches to handle this scenario.'); + 'in_app_purchase]: (Fatal)The observer has not been set but we received a purchase transaction notification. Please ensure the observer has been set using `setTransactionObserver`. Make sure the observer is added right at the App Launch.'); switch (call.method) { case 'updatedTransactions': { final List transactions = _getTransactionList(call.arguments); - return await Future(() { + return Future(() { _observer.updatedTransactions(transactions: transactions); }); } @@ -93,20 +93,20 @@ class SKPaymentQueueWrapper { { final List transactions = _getTransactionList(call.arguments); - return await Future(() { + return Future(() { _observer.removedTransactions(transactions: transactions); }); } case 'restoreCompletedTransactions': { final Error error = call.arguments; - return await Future(() { + return Future(() { _observer.restoreCompletedTransactions(error: error); }); } case 'paymentQueueRestoreCompletedTransactionsFinished': { - return await Future(() { + return Future(() { _observer.paymentQueueRestoreCompletedTransactionsFinished(); }); } @@ -114,7 +114,7 @@ class SKPaymentQueueWrapper { { final List downloads = _getDownloadList(call.arguments); - return await Future(() { + return Future(() { _observer.updatedDownloads(downloads: downloads); }); } @@ -124,7 +124,7 @@ class SKPaymentQueueWrapper { SKPaymentWrapper.fromJson(call.arguments['payment']); SKProductWrapper product = SKProductWrapper.fromJson(call.arguments['product']); - return await Future(() { + return Future(() { if (_observer.shouldAddStorePayment( payment: payment, product: product) == true) { @@ -140,9 +140,8 @@ class SKPaymentQueueWrapper { // Get transaction wrapper object list from arguments. List _getTransactionList(dynamic arguments) { - final List> transactionsMap = arguments; - final List transactions = transactionsMap - .map((Map map) => + final List transactions = arguments + .map((dynamic map) => SKPaymentTransactionWrapper.fromJson(map)) .toList(); return transactions; @@ -150,10 +149,9 @@ class SKPaymentQueueWrapper { // Get download wrapper object list from arguments. List _getDownloadList(dynamic arguments) { - final List> downloadsMap = arguments; - final List downloads = downloadsMap + final List downloads = arguments .map( - (Map map) => SKDownloadWrapper.fromJson(map)) + (dynamic map) => SKDownloadWrapper.fromJson(map)) .toList(); return downloads; } @@ -219,7 +217,7 @@ enum SKPaymentTransactionStateWrapper { /// /// Created when a payment is added to the [SKPaymentQueueWrapper]. Transactions are delivered to your app when a payment is finished processing. /// Completed transactions provide a receipt and a transaction identifier that the app can use to save a permanent record of the processed payment. -@JsonSerializable() +@JsonSerializable(nullable: true) class SKPaymentTransactionWrapper { SKPaymentTransactionWrapper({ @required this.payment, @@ -319,7 +317,7 @@ enum SKDownloadState { /// Note that all downloaded files must be processed before the completion of the [SKPaymentTransactionWrapper]([SKPaymentQueueWrapper.finishTransaction] is called). /// After the transaction is complete, any [SKDownloadWrapper] object in the transaction will not be able to be added to the payment queue /// and the [contentURL ]of the [SKDownloadWrapper] object will be invalid. -@JsonSerializable() +@JsonSerializable(nullable: true) class SKDownloadWrapper { SKDownloadWrapper({ @required this.contentIdentifier, @@ -383,7 +381,7 @@ class SKDownloadWrapper { } /// Dart wrapper around StoreKit's [NSError](https://developer.apple.com/documentation/foundation/nserror?language=objc). -@JsonSerializable() +@JsonSerializable(nullable: true) class SKError { SKError( {@required this.code, @required this.domain, @required this.userInfo}); @@ -414,7 +412,7 @@ class SKError { /// Used as the parameter to initiate a payment. /// In general, a developer should not need to create the payment object explicitly; instead, use /// [SKPaymentQueueWrapper.addPayment] directly with a product identifier to initiate a payment. -@JsonSerializable() +@JsonSerializable(nullable: true) class SKPaymentWrapper { SKPaymentWrapper( {@required this.productIdentifier, From 6989936103d90b99b6291e9020b8cb9a6fef5dfa Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 22 Feb 2019 14:54:48 -0800 Subject: [PATCH 46/64] refresh serializer --- .../sk_payment_queue_wrapper.g.dart | 42 +++++++++++++------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart index 67e81bd514fe..6853e54a6be1 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart @@ -8,17 +8,23 @@ part of 'sk_payment_queue_wrapper.dart'; SKPaymentTransactionWrapper _$SKPaymentTransactionWrapperFromJson(Map json) { return SKPaymentTransactionWrapper( - payment: SKPaymentWrapper.fromJson(json['payment'] as Map), - transactionState: _$enumDecode( + payment: json['payment'] == null + ? null + : SKPaymentWrapper.fromJson(json['payment'] as Map), + transactionState: _$enumDecodeNullable( _$SKPaymentTransactionStateWrapperEnumMap, json['transactionState']), - originalTransaction: SKPaymentTransactionWrapper.fromJson( - json['originalTransaction'] as Map), - transactionTimeStamp: (json['transactionTimeStamp'] as num).toDouble(), + originalTransaction: json['originalTransaction'] == null + ? null + : SKPaymentTransactionWrapper.fromJson( + json['originalTransaction'] as Map), + transactionTimeStamp: (json['transactionTimeStamp'] as num)?.toDouble(), transactionIdentifier: json['transactionIdentifier'] as String, downloads: (json['downloads'] as List) - .map((e) => SKDownloadWrapper.fromJson(e as Map)) - .toList(), - error: SKError.fromJson(json['error'] as Map)); + ?.map((e) => e == null ? null : SKDownloadWrapper.fromJson(e as Map)) + ?.toList(), + error: json['error'] == null + ? null + : SKError.fromJson(json['error'] as Map)); } T _$enumDecode(Map enumValues, dynamic source) { @@ -34,6 +40,13 @@ T _$enumDecode(Map enumValues, dynamic source) { .key; } +T _$enumDecodeNullable(Map enumValues, dynamic source) { + if (source == null) { + return null; + } + return _$enumDecode(enumValues, source); +} + const _$SKPaymentTransactionStateWrapperEnumMap = { SKPaymentTransactionStateWrapper.purchasing: 0, @@ -46,15 +59,17 @@ const _$SKPaymentTransactionStateWrapperEnumMap = SKDownloadWrapper _$SKDownloadWrapperFromJson(Map json) { return SKDownloadWrapper( contentIdentifier: json['contentIdentifier'] as String, - state: _$enumDecode(_$SKDownloadStateEnumMap, json['state']), + state: _$enumDecodeNullable(_$SKDownloadStateEnumMap, json['state']), contentLength: json['contentLength'] as int, contentURL: json['contentURL'] as String, contentVersion: json['contentVersion'] as String, transactionID: json['transactionID'] as String, - progress: (json['progress'] as num).toDouble(), - timeRemaining: (json['timeRemaining'] as num).toDouble(), + progress: (json['progress'] as num)?.toDouble(), + timeRemaining: (json['timeRemaining'] as num)?.toDouble(), downloadTimeUnknown: json['downloadTimeUnknown'] as bool, - error: SKError.fromJson(json['error'] as Map)); + error: json['error'] == null + ? null + : SKError.fromJson(json['error'] as Map)); } const _$SKDownloadStateEnumMap = { @@ -70,7 +85,8 @@ SKError _$SKErrorFromJson(Map json) { return SKError( code: json['code'] as int, domain: json['domain'] as String, - userInfo: Map.from(json['userInfo'] as Map)); + userInfo: + (json['userInfo'] as Map)?.map((k, e) => MapEntry(k as String, e))); } SKPaymentWrapper _$SKPaymentWrapperFromJson(Map json) { From 693ed903c49bef393477005f6fd01c935fd77a31 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 22 Feb 2019 14:56:57 -0800 Subject: [PATCH 47/64] formatting --- .../lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index 522eb01fff8d..479c2efebabe 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -141,8 +141,8 @@ class SKPaymentQueueWrapper { // Get transaction wrapper object list from arguments. List _getTransactionList(dynamic arguments) { final List transactions = arguments - .map((dynamic map) => - SKPaymentTransactionWrapper.fromJson(map)) + .map( + (dynamic map) => SKPaymentTransactionWrapper.fromJson(map)) .toList(); return transactions; } From 69cd34c37b1b4edf23397aa09849b6c486f35d7e Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 1 Mar 2019 13:12:35 -0800 Subject: [PATCH 48/64] formatting --- .../InAppPurchasePluginTest.m | 19 ++++++++++--------- .../ios/Classes/FIAPaymentQueueHandler.m | 1 - 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m index 4180bfadff47..fcfc3133291f 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m @@ -152,7 +152,7 @@ - (void)testRestoreTransactions { SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; queue.testState = SKPaymentTransactionStatePurchased; __block BOOL callbackInvoked = NO; - self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue transactionsUpdated:^(NSArray* _Nonnull transactions) { } transactionRemoved:nil @@ -160,14 +160,15 @@ - (void)testRestoreTransactions { restoreCompletedTransactionsFinished:^() { callbackInvoked = YES; [expectation fulfill]; - } shouldAddStorePayment:nil - updatedDownloads:nil]; - [queue addTransactionObserver:self.plugin.paymentQueueHandler]; - [self.plugin handleMethodCall:call - result:^(id r){ - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertTrue(callbackInvoked); + } + shouldAddStorePayment:nil + updatedDownloads:nil]; + [queue addTransactionObserver:self.plugin.paymentQueueHandler]; + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertTrue(callbackInvoked); } @end diff --git a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m index 152a7a54e809..c785178cd387 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m +++ b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m @@ -60,7 +60,6 @@ - (void)restoreTransactions:(nullable NSString *)applicationName { } } - #pragma mark - observing // Sent when the transaction array has changed (additions or state changes). Client should check // state of transactions and finish as appropriate. From 31ef6301a681c1e101e40efd2cbc94cd11e57ae8 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 1 Mar 2019 15:48:25 -0800 Subject: [PATCH 49/64] update doc --- .../store_kit_wrappers/sk_payment_queue_wrapper.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index b2f8ac17209c..53a9d0e40eae 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -81,14 +81,18 @@ class SKPaymentQueueWrapper { /// Restore transactions to maintain access to content that customers have already purchased. /// - /// For example, when a user upgrade to a new phone, they want to keep the content they purchased in the old phone. + /// For example, when a user upgrades to a new phone, they want to keep the content they purchased in the old phone. /// This call will invoke the [SKTransactionObserverWrapper.restoreCompletedTransactions] or [SKTransactionObserverWrapper.paymentQueueRestoreCompletedTransactionsFinished] as well as [SKTransactionObserverWrapper.updatedTransaction] /// in the [SKTransactionObserverWrapper]. If you keep the download content in your own server, in the observer methods, you can simply finish the transaction by calling [finishTransaction] and /// download the content from your own server. /// If you keep the download content on Apple's server, you can access the download content in the transaction object that you get from [SKTransactionObserverWrapper.updatedTransaction] when the [SKPaymentTransactionWrapper.transactionState] is [SKPaymentTransactionStateWrapper.restored]. - Future restoreTransactions({String applicationName}) async { + /// The `applicationUserName` is the [SKPaymentWrapper.applicationUsername] you used to create payments. If you did not use a `applicationUserName` when creating payments, then you can ignore this parameter. + /// This method either triggers [`-[SKPayment restoreCompletedTransactions]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506123-restorecompletedtransactions?language=objc) or [`-[SKPayment restoreCompletedTransactionsWithApplicationUsername:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1505992-restorecompletedtransactionswith?language=objc) + /// depends on weather the `applicationUserName` is passed. + Future restoreTransactions({String applicationUserName}) async { await channel.invokeMethod( - '-[InAppPurchasePlugin restoreTransactions:result:]', applicationName); + '-[InAppPurchasePlugin restoreTransactions:result:]', + applicationUserName); } // Triage a method channel call from the platform and triggers the correct observer method. From c0e6eeb0d6c4a85812ed7c06d99d2e317ca3aa8c Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 1 Mar 2019 16:38:39 -0800 Subject: [PATCH 50/64] add retrive receipt method in objc --- .../InAppPurchasePluginTest.m | 17 ++++++++++ .../ios/in_app_purchase_pluginTests/Stubs.m | 6 ++++ .../ios/Classes/InAppPurchasePlugin.m | 34 +++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m index fcfc3133291f..864ab868dce6 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m @@ -171,4 +171,21 @@ - (void)testRestoreTransactions { XCTAssertTrue(callbackInvoked); } +- (void)testRetriveReceiptData { + XCTestExpectation* expectation = + [self expectationWithDescription:@"receipt data retrived"]; + FlutterMethodCall* call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin retriveReceiptData:result:]" + arguments:nil]; + __block NSDictionary *result; + [self.plugin handleMethodCall:call + result:^(id r){ + result = r; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + NSLog(@"%@", result); + XCTAssertNotNil(result[@"base64data"]); +} + @end diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m index 02d9ea6c8385..f95f23d97d24 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m @@ -134,6 +134,12 @@ - (SKProduct *)getProduct:(NSString *)productID { return [SKProduct new]; } +- (NSData *)getReceiptData:(NSURL *)url { + NSString *originalString = [NSString stringWithFormat:@"test"]; + NSData *data = [[NSData alloc] initWithBase64EncodedString:originalString options:kNilOptions]; + return data; +} + @end @interface SKPaymentQueueStub () diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index c139679932bf..c6e2046b0734 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -79,6 +79,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [self finishTransaction:call result:result]; } else if ([@"-[InAppPurchasePlugin restoreTransactions:result:]" isEqualToString:call.method]) { [self restoreTransactions:call result:result]; + } else if ([@"-[InAppPurchasePlugin retriveReceiptData:result:]" isEqualToString:call.method]) { + [self retriveReceiptData:call result:result]; } else { result(FlutterMethodNotImplemented); } @@ -206,6 +208,34 @@ - (void)restoreTransactions:(FlutterMethodCall *)call result:(FlutterResult)resu [self.paymentQueueHandler restoreTransactions:call.arguments]; } +- (void)retriveReceiptData:(FlutterMethodCall *)call result:(FlutterResult)result { + BOOL serilized = [call.arguments boolValue]; + NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; + NSData *receipt = [self getReceiptData:receiptURL]; + if (!receipt) { + result([FlutterError + errorWithCode:@"storekit_no_receipt" + message:@"Cannot find receipt for the current main bundle." + details:call.arguments]); + return; + } + NSDictionary *returnMap; + if (serilized) { + NSError *error = nil; + returnMap = [NSJSONSerialization JSONObjectWithData:receipt options:kNilOptions error:&error]; + if (error) { + result([FlutterError + errorWithCode:@"storekit_retrive_receipt_json_serialization_error" + message:error.domain + details:error.userInfo]); + return; + } + result(returnMap); + } else { + result(@{@"base64data":[receipt base64EncodedStringWithOptions:kNilOptions]}); + } +} + #pragma mark - delegates - (void)handleTransactionsUpdated:(NSArray *)transactions { @@ -267,6 +297,10 @@ - (SKProduct *)getProduct:(NSString *)productID { return [self.productsCache objectForKey:productID]; } +- (NSData *)getReceiptData:(NSURL *)url { + return [NSData dataWithContentsOfURL:url]; +} + #pragma mark - getter - (NSSet *)requestHandlers { From 2729eee869d900c4e460a52ed56ba319e75779d0 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 1 Mar 2019 16:59:51 -0800 Subject: [PATCH 51/64] retrive receipt data on dart --- .../InAppPurchasePluginTest.m | 6 +++--- .../ios/Classes/InAppPurchasePlugin.m | 12 ++++++------ .../sk_payment_queue_wrapper.dart | 16 ++++++++++++++++ .../sk_payment_queue_wrapper_test.dart | 9 +++++++++ 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m index 864ab868dce6..3fea6882a84a 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m @@ -171,11 +171,11 @@ - (void)testRestoreTransactions { XCTAssertTrue(callbackInvoked); } -- (void)testRetriveReceiptData { +- (void)testretrieveReceiptData { XCTestExpectation* expectation = - [self expectationWithDescription:@"receipt data retrived"]; + [self expectationWithDescription:@"receipt data retrieved"]; FlutterMethodCall* call = [FlutterMethodCall - methodCallWithMethodName:@"-[InAppPurchasePlugin retriveReceiptData:result:]" + methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]" arguments:nil]; __block NSDictionary *result; [self.plugin handleMethodCall:call diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index c6e2046b0734..999c575aff2b 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -79,8 +79,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [self finishTransaction:call result:result]; } else if ([@"-[InAppPurchasePlugin restoreTransactions:result:]" isEqualToString:call.method]) { [self restoreTransactions:call result:result]; - } else if ([@"-[InAppPurchasePlugin retriveReceiptData:result:]" isEqualToString:call.method]) { - [self retriveReceiptData:call result:result]; + } else if ([@"-[InAppPurchasePlugin retrieveReceiptData:result:]" isEqualToString:call.method]) { + [self retrieveReceiptData:call result:result]; } else { result(FlutterMethodNotImplemented); } @@ -208,8 +208,8 @@ - (void)restoreTransactions:(FlutterMethodCall *)call result:(FlutterResult)resu [self.paymentQueueHandler restoreTransactions:call.arguments]; } -- (void)retriveReceiptData:(FlutterMethodCall *)call result:(FlutterResult)result { - BOOL serilized = [call.arguments boolValue]; +- (void)retrieveReceiptData:(FlutterMethodCall *)call result:(FlutterResult)result { + BOOL serialized = [call.arguments boolValue]; NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; NSData *receipt = [self getReceiptData:receiptURL]; if (!receipt) { @@ -220,12 +220,12 @@ - (void)retriveReceiptData:(FlutterMethodCall *)call result:(FlutterResult)resul return; } NSDictionary *returnMap; - if (serilized) { + if (serialized) { NSError *error = nil; returnMap = [NSJSONSerialization JSONObjectWithData:receipt options:kNilOptions error:&error]; if (error) { result([FlutterError - errorWithCode:@"storekit_retrive_receipt_json_serialization_error" + errorWithCode:@"storekit_retrieve_receipt_json_serialization_error" message:error.domain details:error.userInfo]); return; diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index 53a9d0e40eae..53f2243c50b3 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -95,6 +95,22 @@ class SKPaymentQueueWrapper { applicationUserName); } + + /// Retrieve the receipt data from your application's main bundle. + /// + /// If `serialized` is `false`, the receipt data will be contained in a base64 string inside a map: {"base64data":} + /// If `serialized` is `true`, the receipt data will be represented as a Map object. + /// The default value of `serialized` is `false`. + /// You can use the receipt data retrieved by this method to validate users' purchases. + /// There are 2 ways to do so. Either validate locally or validate with App Store. + /// To validate the receipt locally, you will need the detailed receipt information that being represented in a Map. Thus you need to pass `serialized` as true. + /// For more details on how to validate the receipt data, you can refer to Apple's document about [`About Receipt Validation`](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Introduction.html#//apple_ref/doc/uid/TP40010573-CH105-SW1). + Future> retrieveReceiptData({bool serialized = false}) async { + return await Map.castFrom(await channel.invokeMethod( + '-[InAppPurchasePlugin retrieveReceiptData:result:]', + serialized)); + } + // Triage a method channel call from the platform and triggers the correct observer method. Future _handleObserverCallbacks(MethodCall call) { assert(_observer != null, diff --git a/packages/in_app_purchase/test/store_kit_wrappers/sk_payment_queue_wrapper_test.dart b/packages/in_app_purchase/test/store_kit_wrappers/sk_payment_queue_wrapper_test.dart index fc342dbbc759..f4a59a0f3f90 100644 --- a/packages/in_app_purchase/test/store_kit_wrappers/sk_payment_queue_wrapper_test.dart +++ b/packages/in_app_purchase/test/store_kit_wrappers/sk_payment_queue_wrapper_test.dart @@ -68,6 +68,15 @@ void main() { }); }); + group('retreiveReceiptData', () { + test('Should get result', () async { + stubPlatform.addResponse( + name: '-[InAppPurchasePlugin retrieveReceiptData:result:]', value: {'base64Data':'dummy data'}); + final Map result = await SKPaymentQueueWrapper().retrieveReceiptData(); + expect(result['base64Data'], 'dummy data'); + }); + }); + group('Wrapper fromJson tests', () { test('Should construct correct SKPaymentWrapper from json', () { SKPaymentWrapper payment = From d8b788df6c4941abc7d1503caabcd74e42327f23 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 1 Mar 2019 17:00:42 -0800 Subject: [PATCH 52/64] retrive receipt method can be static --- .../lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart | 2 +- .../test/store_kit_wrappers/sk_payment_queue_wrapper_test.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index 53f2243c50b3..fef9c5e1e56f 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -105,7 +105,7 @@ class SKPaymentQueueWrapper { /// There are 2 ways to do so. Either validate locally or validate with App Store. /// To validate the receipt locally, you will need the detailed receipt information that being represented in a Map. Thus you need to pass `serialized` as true. /// For more details on how to validate the receipt data, you can refer to Apple's document about [`About Receipt Validation`](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Introduction.html#//apple_ref/doc/uid/TP40010573-CH105-SW1). - Future> retrieveReceiptData({bool serialized = false}) async { + static Future> retrieveReceiptData({bool serialized = false}) async { return await Map.castFrom(await channel.invokeMethod( '-[InAppPurchasePlugin retrieveReceiptData:result:]', serialized)); diff --git a/packages/in_app_purchase/test/store_kit_wrappers/sk_payment_queue_wrapper_test.dart b/packages/in_app_purchase/test/store_kit_wrappers/sk_payment_queue_wrapper_test.dart index f4a59a0f3f90..40fae44d4564 100644 --- a/packages/in_app_purchase/test/store_kit_wrappers/sk_payment_queue_wrapper_test.dart +++ b/packages/in_app_purchase/test/store_kit_wrappers/sk_payment_queue_wrapper_test.dart @@ -72,7 +72,7 @@ void main() { test('Should get result', () async { stubPlatform.addResponse( name: '-[InAppPurchasePlugin retrieveReceiptData:result:]', value: {'base64Data':'dummy data'}); - final Map result = await SKPaymentQueueWrapper().retrieveReceiptData(); + final Map result = await SKPaymentQueueWrapper.retrieveReceiptData(); expect(result['base64Data'], 'dummy data'); }); }); From c0c45c1535edbdaa5f907503f32c438439ffd9ee Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Sat, 2 Mar 2019 09:29:17 -0800 Subject: [PATCH 53/64] receipt manager --- .../InAppPurchasePluginTest.m | 30 +++---- .../ios/in_app_purchase_pluginTests/Stubs.h | 4 + .../ios/in_app_purchase_pluginTests/Stubs.m | 15 ++-- .../ios/Classes/FIAPReceiptManager.h | 17 ++++ .../ios/Classes/FIAPReceiptManager.m | 42 ++++++++++ .../ios/Classes/InAppPurchasePlugin.h | 3 + .../ios/Classes/InAppPurchasePlugin.m | 78 ++++++++++++------- .../sk_payment_queue_wrapper.dart | 15 ---- .../sk_receipt_handler.dart | 25 ++++++ .../sk_payment_queue_wrapper_test.dart | 6 +- 10 files changed, 167 insertions(+), 68 deletions(-) create mode 100644 packages/in_app_purchase/ios/Classes/FIAPReceiptManager.h create mode 100644 packages/in_app_purchase/ios/Classes/FIAPReceiptManager.m create mode 100644 packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_handler.dart diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m index 3fea6882a84a..87f2791a0937 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m @@ -16,7 +16,8 @@ @interface InAppPurchasePluginTest : XCTestCase @implementation InAppPurchasePluginTest - (void)setUp { - self.plugin = [[InAppPurchasePluginStub alloc] init]; + self.plugin = + [[InAppPurchasePluginStub alloc] initWithReceiptManager:[FIAPReceiptManagerStub new]]; } - (void)tearDown { @@ -172,20 +173,19 @@ - (void)testRestoreTransactions { } - (void)testretrieveReceiptData { - XCTestExpectation* expectation = - [self expectationWithDescription:@"receipt data retrieved"]; - FlutterMethodCall* call = [FlutterMethodCall - methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]" - arguments:nil]; - __block NSDictionary *result; - [self.plugin handleMethodCall:call - result:^(id r){ - result = r; - [expectation fulfill]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - NSLog(@"%@", result); - XCTAssertNotNil(result[@"base64data"]); + XCTestExpectation* expectation = [self expectationWithDescription:@"receipt data retrieved"]; + FlutterMethodCall* call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]" + arguments:nil]; + __block NSDictionary* result; + [self.plugin handleMethodCall:call + result:^(id r) { + result = r; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + NSLog(@"%@", result); + XCTAssertNotNil(result[@"base64data"]); } @end diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.h b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.h index 9ea6e6a54868..8d6d3e389595 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.h +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.h @@ -4,6 +4,7 @@ #import #import +#import "FIAPReceiptManager.h" #import "FIAPRequestHandler.h" #import "InAppPurchasePlugin.h" @@ -53,4 +54,7 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithMap:(NSDictionary *)map; @end +@interface FIAPReceiptManagerStub : FIAPReceiptManager +@end + NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m index f95f23d97d24..78b0139b346b 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m @@ -134,12 +134,6 @@ - (SKProduct *)getProduct:(NSString *)productID { return [SKProduct new]; } -- (NSData *)getReceiptData:(NSURL *)url { - NSString *originalString = [NSString stringWithFormat:@"test"]; - NSData *data = [[NSData alloc] initWithBase64EncodedString:originalString options:kNilOptions]; - return data; -} - @end @interface SKPaymentQueueStub () @@ -241,3 +235,12 @@ - (instancetype)initWithMap:(NSDictionary *)map { } @end + +@implementation FIAPReceiptManagerStub : FIAPReceiptManager + +- (NSData *)getReceiptData:(NSURL *)url { + NSString *originalString = [NSString stringWithFormat:@"test"]; + return [[NSData alloc] initWithBase64EncodedString:originalString options:kNilOptions]; +} + +@end diff --git a/packages/in_app_purchase/ios/Classes/FIAPReceiptManager.h b/packages/in_app_purchase/ios/Classes/FIAPReceiptManager.h new file mode 100644 index 000000000000..4a916cc0991e --- /dev/null +++ b/packages/in_app_purchase/ios/Classes/FIAPReceiptManager.h @@ -0,0 +1,17 @@ +// Copyright 2019 The Chromium 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 + +NS_ASSUME_NONNULL_BEGIN + +@class FlutterError; + +@interface FIAPReceiptManager : NSObject + +- (nullable NSDictionary *)retrieveReceipt:(BOOL)serialized error:(FlutterError **)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/ios/Classes/FIAPReceiptManager.m b/packages/in_app_purchase/ios/Classes/FIAPReceiptManager.m new file mode 100644 index 000000000000..bc1eaa56b1af --- /dev/null +++ b/packages/in_app_purchase/ios/Classes/FIAPReceiptManager.m @@ -0,0 +1,42 @@ +// +// FIAPReceiptManager.m +// in_app_purchase +// +// Created by Chris Yang on 3/2/19. +// + +#import "FIAPReceiptManager.h" +#import + +@implementation FIAPReceiptManager + +- (NSDictionary *)retrieveReceipt:(BOOL)serialized error:(FlutterError **)error { + NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; + NSData *receipt = [self getReceiptData:receiptURL]; + if (!receipt) { + *error = [FlutterError errorWithCode:@"storekit_no_receipt" + message:@"Cannot find receipt for the current main bundle." + details:nil]; + return nil; + } + NSDictionary *returnMap; + if (serialized) { + NSError *nsError = nil; + returnMap = [NSJSONSerialization JSONObjectWithData:receipt options:kNilOptions error:&nsError]; + if (error) { + *error = [FlutterError errorWithCode:@"storekit_retrieve_receipt_json_serialization_error" + message:nsError.domain + details:nsError.userInfo]; + return nil; + } + return returnMap; + } else { + return @{@"base64data" : [receipt base64EncodedStringWithOptions:kNilOptions]}; + } +} + +- (NSData *)getReceiptData:(NSURL *)url { + return [NSData dataWithContentsOfURL:url]; +} + +@end diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.h b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.h index f7abae534fca..67a381d31283 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.h +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.h @@ -4,9 +4,12 @@ #import @class FIAPaymentQueueHandler; +@class FIAPReceiptManager; @interface InAppPurchasePlugin : NSObject @property(strong, nonatomic) FIAPaymentQueueHandler *paymentQueueHandler; +- (instancetype)initWithReceiptManager:(FIAPReceiptManager *)receiptManager; + @end diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index 999c575aff2b..0273a277f054 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -5,6 +5,7 @@ #import "InAppPurchasePlugin.h" #import #import "FIAObjectTranslator.h" +#import "FIAPReceiptManager.h" #import "FIAPRequestHandler.h" #import "FIAPaymentQueueHandler.h" @@ -24,6 +25,8 @@ @interface InAppPurchasePlugin () @property(strong, nonatomic) NSObject *messenger; @property(strong, nonatomic) NSObject *registrar; +@property(strong, nonatomic) FIAPReceiptManager *receiptManager; + @end @implementation InAppPurchasePlugin @@ -36,11 +39,18 @@ + (void)registerWithRegistrar:(NSObject *)registrar { [registrar addMethodCallDelegate:instance channel:channel]; } -- (instancetype)initWithRegistrar:(NSObject *)registrar { +- (instancetype)initWithReceiptManager:(FIAPReceiptManager *)receiptManager { self = [self init]; + self.receiptManager = receiptManager; + return self; +} + +- (instancetype)initWithRegistrar:(NSObject *)registrar { + self = [self initWithReceiptManager:[FIAPReceiptManager new]]; self.registrar = registrar; self.registry = [registrar textures]; self.messenger = [registrar messenger]; + __weak typeof(self) weakSelf = self; self.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueue defaultQueue] @@ -81,6 +91,9 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [self restoreTransactions:call result:result]; } else if ([@"-[InAppPurchasePlugin retrieveReceiptData:result:]" isEqualToString:call.method]) { [self retrieveReceiptData:call result:result]; + } else if ([@"-[InAppPurchasePlugin validateReceiptLocally:result:]" + isEqualToString:call.method]) { + [self validateReceiptLocally:call result:result]; } else { result(FlutterMethodNotImplemented); } @@ -209,31 +222,40 @@ - (void)restoreTransactions:(FlutterMethodCall *)call result:(FlutterResult)resu } - (void)retrieveReceiptData:(FlutterMethodCall *)call result:(FlutterResult)result { - BOOL serialized = [call.arguments boolValue]; - NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; - NSData *receipt = [self getReceiptData:receiptURL]; - if (!receipt) { - result([FlutterError - errorWithCode:@"storekit_no_receipt" - message:@"Cannot find receipt for the current main bundle." - details:call.arguments]); - return; - } - NSDictionary *returnMap; - if (serialized) { - NSError *error = nil; - returnMap = [NSJSONSerialization JSONObjectWithData:receipt options:kNilOptions error:&error]; - if (error) { - result([FlutterError - errorWithCode:@"storekit_retrieve_receipt_json_serialization_error" - message:error.domain - details:error.userInfo]); - return; - } - result(returnMap); - } else { - result(@{@"base64data":[receipt base64EncodedStringWithOptions:kNilOptions]}); - } + BOOL serialized = [call.arguments boolValue]; + FlutterError *error = nil; + NSDictionary *receiptInfo = [self.receiptManager retrieveReceipt:serialized error:&error]; + if (error) { + result(error); + return; + } + result(receiptInfo); +} + +// Following the steps recommanded by Apple to validate the receipt locally. +// https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html#//apple_ref/doc/uid/TP40010573-CH1-SW2 +- (void)validateReceiptLocally:(FlutterMethodCall *)call result:(FlutterResult)result { + // Step 1. locate the receipt. + // NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; + // NSData *receipt = [self getReceiptData:receiptURL]; + // if (!receipt) { + // result(@{ + // @"valid":@NO, + // @"failure_code":@"no_receipt", + // @"details":@"Cannot find receipt for the current main bundle." + // }); + // return; + // } + // NSError *error = nil; + // NSDictionary *returnMap = [NSJSONSerialization JSONObjectWithData:receipt + // options:kNilOptions error:&error]; if (error) { + // result([FlutterError + // errorWithCode:@"storekit_retrieve_receipt_json_serialization_error" + // message:error.domain + // details:error.userInfo]); + // return; + // } + // Step 2. Verify that the receipt is properly signed by Apple. } #pragma mark - delegates @@ -297,10 +319,6 @@ - (SKProduct *)getProduct:(NSString *)productID { return [self.productsCache objectForKey:productID]; } -- (NSData *)getReceiptData:(NSURL *)url { - return [NSData dataWithContentsOfURL:url]; -} - #pragma mark - getter - (NSSet *)requestHandlers { diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index adb4d2ad9bef..53a9d0e40eae 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -95,21 +95,6 @@ class SKPaymentQueueWrapper { applicationUserName); } - /// Retrieve the receipt data from your application's main bundle. - /// - /// If `serialized` is `false`, the receipt data will be contained in a base64 string inside a map: {"base64data":} - /// If `serialized` is `true`, the receipt data will be represented as a Map object. - /// The default value of `serialized` is `false`. - /// You can use the receipt data retrieved by this method to validate users' purchases. - /// There are 2 ways to do so. Either validate locally or validate with App Store. - /// To validate the receipt locally, you will need the detailed receipt information that being represented in a Map. Thus you need to pass `serialized` as true. - /// For more details on how to validate the receipt data, you can refer to Apple's document about [`About Receipt Validation`](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Introduction.html#//apple_ref/doc/uid/TP40010573-CH105-SW1). - static Future> retrieveReceiptData({bool serialized = false}) async { - return await Map.castFrom(await channel.invokeMethod( - '-[InAppPurchasePlugin retrieveReceiptData:result:]', - serialized)); - } - // Triage a method channel call from the platform and triggers the correct observer method. Future _handleObserverCallbacks(MethodCall call) { assert(_observer != null, diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_handler.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_handler.dart new file mode 100644 index 000000000000..0a5a56150514 --- /dev/null +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_handler.dart @@ -0,0 +1,25 @@ +// Copyright 2019 The Chromium 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 'dart:async'; +import 'package:in_app_purchase/src/channel.dart'; + +///This class contains static methods to handle StoreKit receipts. +class ReceiptHandler { + /// Retrieve the receipt data from your application's main bundle. + /// + /// If `serialized` is `false`, the receipt data will be contained in a base64 string inside a map: {"base64data":} + /// If `serialized` is `true`, the receipt data will be represented as a Map object. + /// The default value of `serialized` is `false`. + /// You can use the receipt data retrieved by this method to validate users' purchases. + /// There are 2 ways to do so. Either validate locally or validate with App Store. + /// To validate the receipt locally, you will need the detailed receipt information that being represented in a Map. Thus you need to pass `serialized` as true. + /// For more details on how to validate the receipt data, you can refer to Apple's document about [`About Receipt Validation`](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Introduction.html#//apple_ref/doc/uid/TP40010573-CH105-SW1). + static Future> retrieveReceiptData( + {bool serialized = false}) async { + return await Map.castFrom( + await channel.invokeMethod( + '-[InAppPurchasePlugin retrieveReceiptData:result:]', serialized)); + } +} diff --git a/packages/in_app_purchase/test/store_kit_wrappers/sk_payment_queue_wrapper_test.dart b/packages/in_app_purchase/test/store_kit_wrappers/sk_payment_queue_wrapper_test.dart index 40fae44d4564..83bcff430a4b 100644 --- a/packages/in_app_purchase/test/store_kit_wrappers/sk_payment_queue_wrapper_test.dart +++ b/packages/in_app_purchase/test/store_kit_wrappers/sk_payment_queue_wrapper_test.dart @@ -71,8 +71,10 @@ void main() { group('retreiveReceiptData', () { test('Should get result', () async { stubPlatform.addResponse( - name: '-[InAppPurchasePlugin retrieveReceiptData:result:]', value: {'base64Data':'dummy data'}); - final Map result = await SKPaymentQueueWrapper.retrieveReceiptData(); + name: '-[InAppPurchasePlugin retrieveReceiptData:result:]', + value: {'base64Data': 'dummy data'}); + final Map result = + await SKPaymentQueueWrapper.retrieveReceiptData(); expect(result['base64Data'], 'dummy data'); }); }); From ced1bceb2b3876e2fc0c3ac647f44c1d31bb2e38 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Mon, 4 Mar 2019 14:27:52 -0800 Subject: [PATCH 54/64] only get receipt data --- .../InAppPurchasePluginTest.m | 2 +- .../ios/Classes/FIAPReceiptManager.h | 2 +- .../ios/Classes/FIAPReceiptManager.m | 17 +------- .../ios/Classes/InAppPurchasePlugin.m | 40 ++++--------------- .../sk_receipt_handler.dart | 25 ------------ .../sk_payment_queue_wrapper_test.dart | 11 ----- 6 files changed, 12 insertions(+), 85 deletions(-) delete mode 100644 packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_handler.dart diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m index 87f2791a0937..8df6093c08f1 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m @@ -185,7 +185,7 @@ - (void)testretrieveReceiptData { }]; [self waitForExpectations:@[ expectation ] timeout:5]; NSLog(@"%@", result); - XCTAssertNotNil(result[@"base64data"]); + XCTAssertNotNil(result); } @end diff --git a/packages/in_app_purchase/ios/Classes/FIAPReceiptManager.h b/packages/in_app_purchase/ios/Classes/FIAPReceiptManager.h index 4a916cc0991e..2e7d4e55212e 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPReceiptManager.h +++ b/packages/in_app_purchase/ios/Classes/FIAPReceiptManager.h @@ -10,7 +10,7 @@ NS_ASSUME_NONNULL_BEGIN @interface FIAPReceiptManager : NSObject -- (nullable NSDictionary *)retrieveReceipt:(BOOL)serialized error:(FlutterError **)error; +- (NSString *)retrieveReceiptWithError:(FlutterError **)error; @end diff --git a/packages/in_app_purchase/ios/Classes/FIAPReceiptManager.m b/packages/in_app_purchase/ios/Classes/FIAPReceiptManager.m index bc1eaa56b1af..20043e2a3237 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPReceiptManager.m +++ b/packages/in_app_purchase/ios/Classes/FIAPReceiptManager.m @@ -10,7 +10,7 @@ @implementation FIAPReceiptManager -- (NSDictionary *)retrieveReceipt:(BOOL)serialized error:(FlutterError **)error { +- (NSString *)retrieveReceiptWithError:(FlutterError **)error { NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; NSData *receipt = [self getReceiptData:receiptURL]; if (!receipt) { @@ -19,20 +19,7 @@ - (NSDictionary *)retrieveReceipt:(BOOL)serialized error:(FlutterError **)error details:nil]; return nil; } - NSDictionary *returnMap; - if (serialized) { - NSError *nsError = nil; - returnMap = [NSJSONSerialization JSONObjectWithData:receipt options:kNilOptions error:&nsError]; - if (error) { - *error = [FlutterError errorWithCode:@"storekit_retrieve_receipt_json_serialization_error" - message:nsError.domain - details:nsError.userInfo]; - return nil; - } - return returnMap; - } else { - return @{@"base64data" : [receipt base64EncodedStringWithOptions:kNilOptions]}; - } + return [receipt base64EncodedStringWithOptions:kNilOptions]; } - (NSData *)getReceiptData:(NSURL *)url { diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index 0273a277f054..a4b8d3207a01 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -42,6 +42,12 @@ + (void)registerWithRegistrar:(NSObject *)registrar { - (instancetype)initWithReceiptManager:(FIAPReceiptManager *)receiptManager { self = [self init]; self.receiptManager = receiptManager; + FlutterError *error; + NSString *receiptData = [self.receiptManager retrieveReceiptWithError:&error]; + if (error) { + NSLog(@"receipt error: %@",error); + } + NSLog(@"receipt data: %@",receiptData); return self; } @@ -91,9 +97,6 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [self restoreTransactions:call result:result]; } else if ([@"-[InAppPurchasePlugin retrieveReceiptData:result:]" isEqualToString:call.method]) { [self retrieveReceiptData:call result:result]; - } else if ([@"-[InAppPurchasePlugin validateReceiptLocally:result:]" - isEqualToString:call.method]) { - [self validateReceiptLocally:call result:result]; } else { result(FlutterMethodNotImplemented); } @@ -222,40 +225,13 @@ - (void)restoreTransactions:(FlutterMethodCall *)call result:(FlutterResult)resu } - (void)retrieveReceiptData:(FlutterMethodCall *)call result:(FlutterResult)result { - BOOL serialized = [call.arguments boolValue]; FlutterError *error = nil; - NSDictionary *receiptInfo = [self.receiptManager retrieveReceipt:serialized error:&error]; + NSString *receiptData = [self.receiptManager retrieveReceiptWithError:&error]; if (error) { result(error); return; } - result(receiptInfo); -} - -// Following the steps recommanded by Apple to validate the receipt locally. -// https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html#//apple_ref/doc/uid/TP40010573-CH1-SW2 -- (void)validateReceiptLocally:(FlutterMethodCall *)call result:(FlutterResult)result { - // Step 1. locate the receipt. - // NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; - // NSData *receipt = [self getReceiptData:receiptURL]; - // if (!receipt) { - // result(@{ - // @"valid":@NO, - // @"failure_code":@"no_receipt", - // @"details":@"Cannot find receipt for the current main bundle." - // }); - // return; - // } - // NSError *error = nil; - // NSDictionary *returnMap = [NSJSONSerialization JSONObjectWithData:receipt - // options:kNilOptions error:&error]; if (error) { - // result([FlutterError - // errorWithCode:@"storekit_retrieve_receipt_json_serialization_error" - // message:error.domain - // details:error.userInfo]); - // return; - // } - // Step 2. Verify that the receipt is properly signed by Apple. + result(receiptData); } #pragma mark - delegates diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_handler.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_handler.dart deleted file mode 100644 index 0a5a56150514..000000000000 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_handler.dart +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2019 The Chromium 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 'dart:async'; -import 'package:in_app_purchase/src/channel.dart'; - -///This class contains static methods to handle StoreKit receipts. -class ReceiptHandler { - /// Retrieve the receipt data from your application's main bundle. - /// - /// If `serialized` is `false`, the receipt data will be contained in a base64 string inside a map: {"base64data":} - /// If `serialized` is `true`, the receipt data will be represented as a Map object. - /// The default value of `serialized` is `false`. - /// You can use the receipt data retrieved by this method to validate users' purchases. - /// There are 2 ways to do so. Either validate locally or validate with App Store. - /// To validate the receipt locally, you will need the detailed receipt information that being represented in a Map. Thus you need to pass `serialized` as true. - /// For more details on how to validate the receipt data, you can refer to Apple's document about [`About Receipt Validation`](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Introduction.html#//apple_ref/doc/uid/TP40010573-CH105-SW1). - static Future> retrieveReceiptData( - {bool serialized = false}) async { - return await Map.castFrom( - await channel.invokeMethod( - '-[InAppPurchasePlugin retrieveReceiptData:result:]', serialized)); - } -} diff --git a/packages/in_app_purchase/test/store_kit_wrappers/sk_payment_queue_wrapper_test.dart b/packages/in_app_purchase/test/store_kit_wrappers/sk_payment_queue_wrapper_test.dart index 83bcff430a4b..fc342dbbc759 100644 --- a/packages/in_app_purchase/test/store_kit_wrappers/sk_payment_queue_wrapper_test.dart +++ b/packages/in_app_purchase/test/store_kit_wrappers/sk_payment_queue_wrapper_test.dart @@ -68,17 +68,6 @@ void main() { }); }); - group('retreiveReceiptData', () { - test('Should get result', () async { - stubPlatform.addResponse( - name: '-[InAppPurchasePlugin retrieveReceiptData:result:]', - value: {'base64Data': 'dummy data'}); - final Map result = - await SKPaymentQueueWrapper.retrieveReceiptData(); - expect(result['base64Data'], 'dummy data'); - }); - }); - group('Wrapper fromJson tests', () { test('Should construct correct SKPaymentWrapper from json', () { SKPaymentWrapper payment = From 5621c04792d3501b5470b15556c8458db8f29510 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Mon, 4 Mar 2019 14:28:21 -0800 Subject: [PATCH 55/64] formating --- .../ios/Classes/InAppPurchasePlugin.m | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index a4b8d3207a01..9bb64dce0d85 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -42,12 +42,12 @@ + (void)registerWithRegistrar:(NSObject *)registrar { - (instancetype)initWithReceiptManager:(FIAPReceiptManager *)receiptManager { self = [self init]; self.receiptManager = receiptManager; - FlutterError *error; - NSString *receiptData = [self.receiptManager retrieveReceiptWithError:&error]; - if (error) { - NSLog(@"receipt error: %@",error); - } - NSLog(@"receipt data: %@",receiptData); + FlutterError *error; + NSString *receiptData = [self.receiptManager retrieveReceiptWithError:&error]; + if (error) { + NSLog(@"receipt error: %@", error); + } + NSLog(@"receipt data: %@", receiptData); return self; } From 0afd8a9ac0d75cd9a26f6e6ec2ab2c3b1704bc76 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Mon, 4 Mar 2019 14:33:32 -0800 Subject: [PATCH 56/64] remove testing code --- packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index 9bb64dce0d85..f2f6e2a87f11 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -42,12 +42,6 @@ + (void)registerWithRegistrar:(NSObject *)registrar { - (instancetype)initWithReceiptManager:(FIAPReceiptManager *)receiptManager { self = [self init]; self.receiptManager = receiptManager; - FlutterError *error; - NSString *receiptData = [self.receiptManager retrieveReceiptWithError:&error]; - if (error) { - NSLog(@"receipt error: %@", error); - } - NSLog(@"receipt data: %@", receiptData); return self; } From b3e61d520a4ec476f7843296fd81e5e061d8dd68 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Mon, 4 Mar 2019 16:09:53 -0800 Subject: [PATCH 57/64] adding refresh receipt method --- .../InAppPurchasePluginTest.m | 15 +++++++ .../ProductRequestHandlerTest.m | 33 +++++++++++++++ .../ios/in_app_purchase_pluginTests/Stubs.h | 4 ++ .../ios/in_app_purchase_pluginTests/Stubs.m | 30 +++++++++++++- .../ios/Classes/FIAPRequestHandler.h | 2 +- .../ios/Classes/FIAPRequestHandler.m | 9 +++-- .../ios/Classes/InAppPurchasePlugin.m | 40 +++++++++++++++++++ .../sk_receipt_manager.dart | 22 ++++++++++ .../store_kit_wrappers/sk_request_maker.dart | 16 ++++++++ .../sk_receipt_manager_test.dart | 22 ++++++++++ 10 files changed, 188 insertions(+), 5 deletions(-) create mode 100644 packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_manager.dart create mode 100644 packages/in_app_purchase/test/store_kit_wrappers/sk_receipt_manager_test.dart diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m index 8df6093c08f1..4a3409c37628 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m @@ -188,4 +188,19 @@ - (void)testretrieveReceiptData { XCTAssertNotNil(result); } +- (void)testRefreshReceiptRequest { + XCTestExpectation* expectation = [self expectationWithDescription:@"expect success"]; + FlutterMethodCall* call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin refreshReceipt:result:]" + arguments:nil]; + __block BOOL result = NO; + [self.plugin handleMethodCall:call + result:^(id r) { + result = YES; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertTrue(result); +} + @end diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/ProductRequestHandlerTest.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/ProductRequestHandlerTest.m index a0eb4704200b..09228a3cd4e3 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/ProductRequestHandlerTest.m +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/ProductRequestHandlerTest.m @@ -52,4 +52,37 @@ - (void)testRequestHandlerWithProductRequestFailure { XCTAssertNil(response); } +- (void)testRequestHandlerWithRefreshReceiptSuccess { + SKReceiptRefreshRequestStub *request = + [[SKReceiptRefreshRequestStub alloc] initWithReceiptProperties:nil]; + FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; + XCTestExpectation *expectation = [self expectationWithDescription:@"expect no error"]; + __block NSError *e; + [handler + startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable r, NSError *error) { + e = error; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertNil(e); +} + +- (void)testRequestHandlerWithRefreshReceiptFailure { + SKReceiptRefreshRequestStub *request = [[SKReceiptRefreshRequestStub alloc] + initWithFailureError:[NSError errorWithDomain:@"test" code:123 userInfo:@{}]]; + FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; + XCTestExpectation *expectation = [self expectationWithDescription:@"expect error"]; + __block NSError *error; + __block SKProductsResponse *response; + [handler startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable r, NSError *e) { + error = e; + response = r; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertNotNil(error); + XCTAssertEqual(error.domain, @"test"); + XCTAssertNil(response); +} + @end diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.h b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.h index 8d6d3e389595..fe943a40a156 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.h +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.h @@ -57,4 +57,8 @@ NS_ASSUME_NONNULL_BEGIN @interface FIAPReceiptManagerStub : FIAPReceiptManager @end +@interface SKReceiptRefreshRequestStub : SKReceiptRefreshRequest +- (instancetype)initWithFailureError:(NSError *)error; +@end + NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m index 78b0139b346b..338213bce212 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Stubs.m @@ -98,7 +98,6 @@ - (void)start { } else { [self.delegate productsRequest:self didReceiveResponse:response]; } - [self.delegate requestDidFinish:self]; } @end @@ -134,6 +133,10 @@ - (SKProduct *)getProduct:(NSString *)productID { return [SKProduct new]; } +- (SKReceiptRefreshRequestStub *)getRefreshReceiptRequest:(NSDictionary *)properties { + return [[SKReceiptRefreshRequestStub alloc] initWithReceiptProperties:properties]; +} + @end @interface SKPaymentQueueStub () @@ -244,3 +247,28 @@ - (NSData *)getReceiptData:(NSURL *)url { } @end + +@implementation SKReceiptRefreshRequestStub { + NSError *_error; +} + +- (instancetype)initWithReceiptProperties:(NSDictionary *)properties { + self = [super initWithReceiptProperties:properties]; + return self; +} + +- (instancetype)initWithFailureError:(NSError *)error { + self = [super init]; + _error = error; + return self; +} + +- (void)start { + if (_error) { + [self.delegate request:self didFailWithError:_error]; + } else { + [self.delegate requestDidFinish:self]; + } +} + +@end diff --git a/packages/in_app_purchase/ios/Classes/FIAPRequestHandler.h b/packages/in_app_purchase/ios/Classes/FIAPRequestHandler.h index a4f7cb484b5c..892f5f013cc9 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPRequestHandler.h +++ b/packages/in_app_purchase/ios/Classes/FIAPRequestHandler.h @@ -12,7 +12,7 @@ typedef void (^ProductRequestCompletion)(SKProductsResponse *_Nullable response, @interface FIAPRequestHandler : NSObject -- (instancetype)initWithRequest:(SKProductsRequest *)request; +- (instancetype)initWithRequest:(SKRequest *)request; - (void)startProductRequestWithCompletionHandler:(ProductRequestCompletion)completion; @end diff --git a/packages/in_app_purchase/ios/Classes/FIAPRequestHandler.m b/packages/in_app_purchase/ios/Classes/FIAPRequestHandler.m index e3d97e0e4338..b182e3ff94e2 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPRequestHandler.m +++ b/packages/in_app_purchase/ios/Classes/FIAPRequestHandler.m @@ -10,13 +10,13 @@ @interface FIAPRequestHandler () @property(copy, nonatomic) ProductRequestCompletion completion; -@property(strong, nonatomic) SKProductsRequest *request; +@property(strong, nonatomic) SKRequest *request; @end @implementation FIAPRequestHandler -- (instancetype)initWithRequest:(SKProductsRequest *)request { +- (instancetype)initWithRequest:(SKRequest *)request { self = [super init]; if (self) { self.request = request; @@ -34,11 +34,14 @@ - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { if (self.completion) { self.completion(response, nil); + self.completion = nil; } } -// Reserved for other SKRequests. - (void)requestDidFinish:(SKRequest *)request { + if (self.completion) { + self.completion(nil, nil); + } } - (void)request:(SKRequest *)request didFailWithError:(NSError *)error { diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index f2f6e2a87f11..5464eba1aa0b 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -91,6 +91,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [self restoreTransactions:call result:result]; } else if ([@"-[InAppPurchasePlugin retrieveReceiptData:result:]" isEqualToString:call.method]) { [self retrieveReceiptData:call result:result]; + } else if ([@"-[InAppPurchasePlugin refreshReceipt:result:]" isEqualToString:call.method]) { + [self refreshReceipt:call result:result]; } else { result(FlutterMethodNotImplemented); } @@ -228,6 +230,40 @@ - (void)retrieveReceiptData:(FlutterMethodCall *)call result:(FlutterResult)resu result(receiptData); } +- (void)refreshReceipt:(FlutterMethodCall *)call result:(FlutterResult)result { + NSDictionary *arguments = call.arguments; + SKReceiptRefreshRequest *request; + if (arguments) { + if (![arguments isKindOfClass:[NSDictionary class]]) { + result([FlutterError errorWithCode:@"storekit_invalid_argument" + message:@"Argument type of startRequest is not array" + details:call.arguments]); + return; + } + NSMutableDictionary *properties = [NSMutableDictionary new]; + properties[SKReceiptPropertyIsExpired] = arguments[@"isExpired"]; + properties[SKReceiptPropertyIsRevoked] = arguments[@"isRevoked"]; + properties[SKReceiptPropertyIsVolumePurchase] = arguments[@"isVolumePurchase"]; + request = [self getRefreshReceiptRequest:properties]; + } else { + request = [self getRefreshReceiptRequest:nil]; + } + FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; + [self.requestHandlers addObject:handler]; + __weak typeof(self) weakSelf = self; + [handler startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable response, + NSError *_Nullable error) { + if (error) { + result([FlutterError errorWithCode:@"storekit_refreshreceiptrequest_platform_error" + message:error.description + details:error.userInfo]); + return; + } + result(nil); + [weakSelf.requestHandlers removeObject:handler]; + }]; +} + #pragma mark - delegates - (void)handleTransactionsUpdated:(NSArray *)transactions { @@ -289,6 +325,10 @@ - (SKProduct *)getProduct:(NSString *)productID { return [self.productsCache objectForKey:productID]; } +- (SKReceiptRefreshRequest *)getRefreshReceiptRequest:(NSDictionary *)properties { + return [[SKReceiptRefreshRequest alloc] initWithReceiptProperties:properties]; +} + #pragma mark - getter - (NSSet *)requestHandlers { diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_manager.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_manager.dart new file mode 100644 index 000000000000..a8187fe011d9 --- /dev/null +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_manager.dart @@ -0,0 +1,22 @@ +// Copyright 2019 The Chromium 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 'dart:async'; +import 'package:in_app_purchase/src/channel.dart'; + + ///This class contains static methods to manage StoreKit receipts. +class SKReceiptManager { + /// Retrieve the receipt data from your application's main bundle. + /// + /// The receipt data will be based64 encoded. The structure of the payload is defined using ASN.1. + /// You can use the receipt data retrieved by this method to validate users' purchases. + /// There are 2 ways to do so. Either validate locally or validate with App Store. + /// For more details on how to validate the receipt data, you can refer to Apple's document about [`About Receipt Validation`](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Introduction.html#//apple_ref/doc/uid/TP40010573-CH105-SW1). + /// If the receipt is invalid or missing, you can use [SKRequestMaker.startRefreshReceiptRequest] to request a new receipt. + Future retrieveReceiptData( + {bool serialized = false}) { + return channel.invokeMethod( + '-[InAppPurchasePlugin retrieveReceiptData:result:]'); + } +} \ No newline at end of file diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_request_maker.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_request_maker.dart index 295e7277315a..51cc3959f211 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_request_maker.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_request_maker.dart @@ -36,4 +36,20 @@ class SKRequestMaker { } return SkProductResponseWrapper.fromJson(productResponseMap); } + + /// Uses [SKReceiptRefreshRequest](https://developer.apple.com/documentation/storekit/skreceiptrefreshrequest?language=objc) to request a new receipt. + /// + /// If the receipt is invalid or missing, you can use this API to request a new receipt. + /// The [receiptProperties] is optional and it exists only for [sandbox testing](https://developer.apple.com/apple-pay/sandbox-testing/). In the production app, call this API without pass in the [receiptProperties] parameter. + /// To test in the sandbox, you can request a receipt with any combination of properties to test the state transitions related to [`Volume Purchase Plan`](https://www.apple.com/business/site/docs/VPP_Business_Guide.pdf) receipts. + /// The valid keys in the receiptProperties are below (All of them are of type bool): + /// * isExpired: whether the receipt is expired. + /// * isRevoked: whether the receipt has been revoked. + /// * isExpired: whether the receipt is a Volume Purchase Plan receipt. + Future startRefreshReceiptRequest({Map receiptProperties}) { + return channel.invokeMethod( + '-[InAppPurchasePlugin refreshReceipt:result:]', + receiptProperties, + ); + } } diff --git a/packages/in_app_purchase/test/store_kit_wrappers/sk_receipt_manager_test.dart b/packages/in_app_purchase/test/store_kit_wrappers/sk_receipt_manager_test.dart new file mode 100644 index 000000000000..4d419811bcf8 --- /dev/null +++ b/packages/in_app_purchase/test/store_kit_wrappers/sk_receipt_manager_test.dart @@ -0,0 +1,22 @@ +import 'package:test/test.dart'; +import 'package:in_app_purchase/src/store_kit_wrappers/sk_receipt_manager.dart'; +import 'package:in_app_purchase/src/channel.dart'; + +import '../stub_in_app_purchase_platform.dart'; + +void main() { + final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform(); + + setUpAll(() => + channel.setMockMethodCallHandler(stubPlatform.fakeMethodCallHandler)); + + group('retrieveReceiptData', () { + test('Should get result', () async { + stubPlatform.addResponse( + name: '-[InAppPurchasePlugin retrieveReceiptData:result:]', + value: 'dummy data'); + final String result = await SKReceiptManager().retrieveReceiptData(); + expect(result, 'dummy data'); + }); + }); +} From 88a49c67c42510b6336b7419fecfa72f6aefb588 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Mon, 4 Mar 2019 16:11:52 -0800 Subject: [PATCH 58/64] formatting --- .../src/store_kit_wrappers/sk_receipt_manager.dart | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_manager.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_manager.dart index a8187fe011d9..a59c3fcea933 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_manager.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_manager.dart @@ -5,7 +5,7 @@ import 'dart:async'; import 'package:in_app_purchase/src/channel.dart'; - ///This class contains static methods to manage StoreKit receipts. +///This class contains static methods to manage StoreKit receipts. class SKReceiptManager { /// Retrieve the receipt data from your application's main bundle. /// @@ -14,9 +14,8 @@ class SKReceiptManager { /// There are 2 ways to do so. Either validate locally or validate with App Store. /// For more details on how to validate the receipt data, you can refer to Apple's document about [`About Receipt Validation`](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Introduction.html#//apple_ref/doc/uid/TP40010573-CH105-SW1). /// If the receipt is invalid or missing, you can use [SKRequestMaker.startRefreshReceiptRequest] to request a new receipt. - Future retrieveReceiptData( - {bool serialized = false}) { - return channel.invokeMethod( - '-[InAppPurchasePlugin retrieveReceiptData:result:]'); + Future retrieveReceiptData({bool serialized = false}) { + return channel + .invokeMethod('-[InAppPurchasePlugin retrieveReceiptData:result:]'); } -} \ No newline at end of file +} From b2d4394a7900a1708b3f424962039d7c4b5254fc Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Mon, 4 Mar 2019 16:12:26 -0800 Subject: [PATCH 59/64] typo fix --- .../ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m index 4a3409c37628..db7c1a88a4c8 100644 --- a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m +++ b/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/InAppPurchasePluginTest.m @@ -172,7 +172,7 @@ - (void)testRestoreTransactions { XCTAssertTrue(callbackInvoked); } -- (void)testretrieveReceiptData { +- (void)testRetrieveReceiptData { XCTestExpectation* expectation = [self expectationWithDescription:@"receipt data retrieved"]; FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]" From 47d5886c4c59443d4b0e86cb4d195b2c905de863 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Mon, 4 Mar 2019 16:16:11 -0800 Subject: [PATCH 60/64] comments --- packages/in_app_purchase/ios/Classes/FIAPRequestHandler.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/in_app_purchase/ios/Classes/FIAPRequestHandler.m b/packages/in_app_purchase/ios/Classes/FIAPRequestHandler.m index b182e3ff94e2..5dc2cea2e9db 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPRequestHandler.m +++ b/packages/in_app_purchase/ios/Classes/FIAPRequestHandler.m @@ -34,6 +34,8 @@ - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { if (self.completion) { self.completion(response, nil); + // set the completion to nil here so self.completion won't be triggered again in + // requestDidFinish for SKProductRequest. self.completion = nil; } } From df7e6f227e581fec0c6aa6041eccf94d9020067d Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Mon, 4 Mar 2019 16:17:24 -0800 Subject: [PATCH 61/64] license header --- .../test/store_kit_wrappers/sk_receipt_manager_test.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/in_app_purchase/test/store_kit_wrappers/sk_receipt_manager_test.dart b/packages/in_app_purchase/test/store_kit_wrappers/sk_receipt_manager_test.dart index 4d419811bcf8..b35fd903b8ec 100644 --- a/packages/in_app_purchase/test/store_kit_wrappers/sk_receipt_manager_test.dart +++ b/packages/in_app_purchase/test/store_kit_wrappers/sk_receipt_manager_test.dart @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium 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 'package:test/test.dart'; import 'package:in_app_purchase/src/store_kit_wrappers/sk_receipt_manager.dart'; import 'package:in_app_purchase/src/channel.dart'; From 94022cb37eefcbeaee14d28408315632f14ed400 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Tue, 5 Mar 2019 12:44:21 -0800 Subject: [PATCH 62/64] export receipt manager --- packages/in_app_purchase/lib/store_kit_wrappers.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/in_app_purchase/lib/store_kit_wrappers.dart b/packages/in_app_purchase/lib/store_kit_wrappers.dart index 49d1b0d163c0..c2a770e6ac8e 100644 --- a/packages/in_app_purchase/lib/store_kit_wrappers.dart +++ b/packages/in_app_purchase/lib/store_kit_wrappers.dart @@ -5,3 +5,4 @@ export 'src/store_kit_wrappers/sk_payment_queue_wrapper.dart'; export 'src/store_kit_wrappers/sk_product_wrapper.dart'; export 'src/store_kit_wrappers/sk_request_maker.dart'; +export 'src/store_kit_wrappers//sk_receipt_manager.dart'; From 37334e1760bda1e0a1cc79723ac19e37d583e083 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Tue, 5 Mar 2019 12:45:55 -0800 Subject: [PATCH 63/64] remove unnecessary parameter --- .../lib/src/store_kit_wrappers/sk_receipt_manager.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_manager.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_manager.dart index a59c3fcea933..eed09f1835ef 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_manager.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_manager.dart @@ -14,7 +14,7 @@ class SKReceiptManager { /// There are 2 ways to do so. Either validate locally or validate with App Store. /// For more details on how to validate the receipt data, you can refer to Apple's document about [`About Receipt Validation`](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Introduction.html#//apple_ref/doc/uid/TP40010573-CH105-SW1). /// If the receipt is invalid or missing, you can use [SKRequestMaker.startRefreshReceiptRequest] to request a new receipt. - Future retrieveReceiptData({bool serialized = false}) { + Future retrieveReceiptData() { return channel .invokeMethod('-[InAppPurchasePlugin retrieveReceiptData:result:]'); } From a92652d591b4a6c94b1fee1681cc045f93fa0acc Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Tue, 5 Mar 2019 14:23:00 -0800 Subject: [PATCH 64/64] dartdoc typo fix --- .../lib/src/store_kit_wrappers/sk_request_maker.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_request_maker.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_request_maker.dart index 51cc3959f211..353d34e5f9be 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_request_maker.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_request_maker.dart @@ -45,7 +45,7 @@ class SKRequestMaker { /// The valid keys in the receiptProperties are below (All of them are of type bool): /// * isExpired: whether the receipt is expired. /// * isRevoked: whether the receipt has been revoked. - /// * isExpired: whether the receipt is a Volume Purchase Plan receipt. + /// * isVolumePurchase: whether the receipt is a Volume Purchase Plan receipt. Future startRefreshReceiptRequest({Map receiptProperties}) { return channel.invokeMethod( '-[InAppPurchasePlugin refreshReceipt:result:]',