Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
3d21006
draft
Feb 15, 2019
73caea7
second draft
Feb 15, 2019
9420db0
finish transaction
Feb 15, 2019
62a9974
adding more comments
Feb 15, 2019
2ec9c58
more comments
Feb 15, 2019
e75411a
let dart intercept the payment from store
Feb 15, 2019
824eee8
create payment method
Feb 15, 2019
98f8d03
rearrange method order
Feb 15, 2019
e1176e5
formatting
Feb 15, 2019
6878875
draft
Feb 19, 2019
8feddbf
draft 2
Feb 19, 2019
2ccfe7e
Merge branch 'master' of github.com:flutter/plugins into iap_make_pay…
Feb 19, 2019
0e8de06
Merge branch 'iap_make_payment_objc' into iap_payment_queue_dart_ios
Feb 19, 2019
ec177e3
add payment
Feb 19, 2019
ab75985
typo fix
Feb 19, 2019
5edec3f
Merge branch 'iap_make_payment_objc' into iap_payment_queue_dart_ios
Feb 19, 2019
ed11706
more comments
Feb 19, 2019
564d13b
remove unused code
Feb 19, 2019
330dc3d
Merge branch 'iap_make_payment_objc' into iap_payment_queue_dart_ios
Feb 19, 2019
b11e3c5
update to weakself
Feb 19, 2019
7f9e5a9
review fixes
Feb 19, 2019
0187f9a
formatting
Feb 20, 2019
ec1f0bb
merge the objective-c branch
Feb 20, 2019
6b2d7ea
rename callback methods
Feb 20, 2019
e7da33a
nit fix
Feb 20, 2019
b756e70
rearrange payment object handling code
Feb 20, 2019
4dd5b14
fix unit test, remove nil check for callback blocks
Feb 20, 2019
f3cdb83
formatting
Feb 20, 2019
7ddbc6c
merge and resolve conflicts
Feb 20, 2019
18f1e82
observer update
Feb 20, 2019
a37123e
assert transaction observer is set
Feb 20, 2019
7d57282
finish transaction method
Feb 20, 2019
da19308
merge master
Feb 21, 2019
ad2f3ee
Merge branch 'master' into iap_payment_queue_dart_ios
Feb 21, 2019
c0eabdd
reslove conflicts
Feb 21, 2019
f11949e
formatting
Feb 21, 2019
bd746bf
fixes
Feb 21, 2019
06f8495
local test
Feb 21, 2019
541c573
add assert message
Feb 21, 2019
a3fe929
fix price precision
Feb 21, 2019
61c66d1
more local test code
Feb 21, 2019
8762cfc
review fixes
Feb 21, 2019
736838e
revert sku_details_wrapper.g
Feb 21, 2019
114b5ce
update price to string to avoid precision lost
Feb 21, 2019
343a976
remove unnecessary print statement
Feb 21, 2019
372fad7
Merge branch 'get_product_list_fix' into local_test
Feb 21, 2019
f845b61
fixes
Feb 21, 2019
36d9d25
test case fixes
Feb 21, 2019
055a88f
Merge branch 'get_product_list_fix' into iap_payment_queue_dart_ios
Feb 21, 2019
087d954
Merge branch 'local_test' into iap_payment_queue_dart_ios
Feb 21, 2019
324d7a9
revert main.dart
Feb 21, 2019
17af41a
Merge branch 'master' into iap_payment_queue_dart_ios
Feb 22, 2019
80fcac1
remove unnecessary method
Feb 22, 2019
670bc84
Merge branch 'master' into iap_payment_queue_dart_ios
Feb 22, 2019
5c8b3c5
remove unnessary addpayment logic
Feb 22, 2019
8132c22
formatting and renaming
Feb 22, 2019
ffaf35c
fix transform map to transactions and downloads
Feb 22, 2019
6989936
refresh serializer
Feb 22, 2019
693ed90
formatting
Feb 22, 2019
57fc26f
more comments
Feb 23, 2019
810fb8b
merge origin
Feb 25, 2019
711c437
formating
Feb 28, 2019
237bf2c
more error message and comments
Feb 28, 2019
de1dd3c
more review fixes
Feb 28, 2019
6a4b1b3
review fix
Feb 28, 2019
b163954
formatting
Feb 28, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ - (SKProductRequestStub *)getProductRequestWithIdentifiers:(NSSet *)identifiers
return [[SKProductRequestStub alloc] initWithProductIdentifiers:identifiers];
}

- (SKProduct *)getProduct:(NSString *)productID {
return [SKProduct new];
}

@end

@interface SKPaymentQueueStub ()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ - (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue
self.shouldAddStorePayment = shouldAddStorePayment;
self.updatedDownloads = updatedDownloads;
self.transactionsSetter = [NSMutableDictionary new];
[queue addTransactionObserver:self];
}
return self;
}
Expand All @@ -57,7 +58,9 @@ - (void)finishTransaction:(SKPaymentTransaction *)transaction {
- (void)paymentQueue:(SKPaymentQueue *)queue
updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
for (SKPaymentTransaction *transaction in transactions) {
[self.transactionsSetter setObject:transaction forKey:transaction.transactionIdentifier];
if (transaction.transactionIdentifier) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this ever be null? Should we handle it like an error if/when it is?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apple handles the transaction identifier strangely, the identifier is null when the state the purchasing. I should probably put a comment on it.

[self.transactionsSetter setObject:transaction forKey:transaction.transactionIdentifier];
}
}
// notify dart through callbacks.
self.transactionsUpdated(transactions);
Expand Down
131 changes: 40 additions & 91 deletions packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ @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;
Expand All @@ -35,27 +32,10 @@ + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)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<FlutterPluginRegistrar> *)registrar {
self = [self init];
self.registrar = registrar;
Expand Down Expand Up @@ -88,6 +68,20 @@ - (instancetype)initWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar
return self;
}

- (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 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]]);
}
Expand Down Expand Up @@ -131,83 +125,36 @@ - (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:@"productID"];
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
NSString *productID = [paymentMap objectForKey:@"productIdentifier"];
// When a product is already fetched, we create a payment object with
// the product to process the payment.
SKProduct *product = [self.productsCache objectForKey:productID];
SKProduct *product = [self getProduct: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;
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 =
payment.simulatesAskToBuyInSandbox =
[[paymentMap objectForKey:@"simulatesAskToBuyInSandBox"] boolValue];
}
[self.paymentQueueHandler addPayment:mutablePayment];
[self.paymentQueueHandler addPayment:payment];
result(nil);
return;
}
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."
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]);
}

Expand All @@ -222,9 +169,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 "
@"exist. Note that if the transactionState is "
@"purchasing, the transactionIdentifier will be "
@"nil(null).",
transaction.transactionIdentifier]
details:call.arguments]);
return;
}
@try {
Expand Down Expand Up @@ -283,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],
Expand All @@ -298,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 {
Expand All @@ -314,11 +270,4 @@ - (NSMutableDictionary *)productsCache {
return _productsCache;
}

- (NSMutableDictionary *)paymentsCache {
if (!_paymentsCache) {
_paymentsCache = [NSMutableDictionary new];
}
return _paymentsCache;
}

@end
3 changes: 3 additions & 0 deletions packages/in_app_purchase/lib/src/channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Loading