diff --git a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md index 82b31a947953..044a1fef4fc5 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.3.9 +* Converts `storefront()`, `transactions()`, `addPayment()`, `canMakePayment` to pigeon. * Updates minimum iOS version to 12.0 and minimum Flutter version to 3.16.6. ## 0.3.8+1 diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAObjectTranslator.h b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAObjectTranslator.h index eb97ceb44754..bf32cc128fc1 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAObjectTranslator.h +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAObjectTranslator.h @@ -4,6 +4,7 @@ #import #import +#import "messages.g.h" NS_ASSUME_NONNULL_BEGIN @@ -56,7 +57,19 @@ NS_ASSUME_NONNULL_BEGIN withError:(NSString *_Nullable *_Nullable)error API_AVAILABLE(ios(12.2)); ++ (nullable SKPaymentTransactionMessage *)convertTransactionToPigeon: + (nullable SKPaymentTransaction *)transaction; + ++ (nullable SKStorefrontMessage *)convertStorefrontToPigeon:(nullable SKStorefront *)storefront + API_AVAILABLE(ios(13.0)); + ++ (nullable SKPaymentDiscountMessage *)convertPaymentDiscountToPigeon: + (nullable SKPaymentDiscount *)discount API_AVAILABLE(ios(12.2)); + ++ (nullable SKPaymentMessage *)convertPaymentToPigeon:(nullable SKPayment *)payment + API_AVAILABLE(ios(12.2)); + ++ (nullable SKErrorMessage *)convertSKErrorToPigeon:(NSError *)error; @end -; NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAObjectTranslator.m b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAObjectTranslator.m index ce0aa2d6f23c..bfc5542af5ec 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAObjectTranslator.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAObjectTranslator.m @@ -288,4 +288,92 @@ + (SKPaymentDiscount *)getSKPaymentDiscountFromMap:(NSDictionary *)map return discount; } ++ (nullable SKPaymentTransactionMessage *)convertTransactionToPigeon: + (nullable SKPaymentTransaction *)transaction API_AVAILABLE(ios(12.2)) { + if (!transaction) { + return nil; + } + SKPaymentTransactionMessage *msg = [SKPaymentTransactionMessage + makeWithPayment:[self convertPaymentToPigeon:transaction.payment] + transactionState:[self convertTransactionStateToPigeon:transaction.transactionState] + originalTransaction:transaction.originalTransaction + ? [self convertTransactionToPigeon:transaction.originalTransaction] + : nil + transactionTimeStamp:[NSNumber numberWithDouble:[transaction.transactionDate + timeIntervalSince1970]] + transactionIdentifier:transaction.transactionIdentifier + error:[self convertSKErrorToPigeon:transaction.error]]; + return msg; +} + ++ (nullable SKErrorMessage *)convertSKErrorToPigeon:(NSError *)error { + NSMutableDictionary *userInfo = [NSMutableDictionary new]; + for (NSErrorUserInfoKey key in error.userInfo) { + id value = error.userInfo[key]; + userInfo[key] = [FIAObjectTranslator encodeNSErrorUserInfo:value]; + } + + SKErrorMessage *msg = [SKErrorMessage makeWithCode:error.code + domain:error.domain + userInfo:userInfo]; + return msg; +} + ++ (SKPaymentTransactionStateMessage)convertTransactionStateToPigeon: + (SKPaymentTransactionState)state { + switch (state) { + case SKPaymentTransactionStatePurchasing: + return SKPaymentTransactionStateMessagePurchasing; + case SKPaymentTransactionStatePurchased: + return SKPaymentTransactionStateMessagePurchased; + case SKPaymentTransactionStateFailed: + return SKPaymentTransactionStateMessageFailed; + case SKPaymentTransactionStateRestored: + return SKPaymentTransactionStateMessageRestored; + case SKPaymentTransactionStateDeferred: + return SKPaymentTransactionStateMessageDeferred; + } +} + ++ (nullable SKPaymentMessage *)convertPaymentToPigeon:(nullable SKPayment *)payment + API_AVAILABLE(ios(12.2)) { + if (!payment) { + return nil; + } + SKPaymentMessage *msg = [SKPaymentMessage + makeWithProductIdentifier:payment.productIdentifier + applicationUsername:payment.applicationUsername + requestData:[[NSString alloc] initWithData:payment.requestData + encoding:NSUTF8StringEncoding] + quantity:payment.quantity + simulatesAskToBuyInSandbox:payment.simulatesAskToBuyInSandbox + paymentDiscount:[self convertPaymentDiscountToPigeon:payment.paymentDiscount]]; + return msg; +} + ++ (nullable SKPaymentDiscountMessage *)convertPaymentDiscountToPigeon: + (nullable SKPaymentDiscount *)discount API_AVAILABLE(ios(12.2)) { + if (!discount) { + return nil; + } + SKPaymentDiscountMessage *msg = + [SKPaymentDiscountMessage makeWithIdentifier:discount.identifier + keyIdentifier:discount.keyIdentifier + nonce:[discount.nonce UUIDString] + signature:discount.signature + timestamp:[discount.timestamp intValue]]; + + return msg; +} + ++ (nullable SKStorefrontMessage *)convertStorefrontToPigeon:(nullable SKStorefront *)storefront + API_AVAILABLE(ios(13.0)) { + if (!storefront) { + return nil; + } + SKStorefrontMessage *msg = [SKStorefrontMessage makeWithCountryCode:storefront.countryCode + identifier:storefront.identifier]; + return msg; +} + @end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.h b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.h index eeab0a706683..a2b9cb0eb934 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.h +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.h @@ -7,10 +7,12 @@ #else #import #endif +#import "messages.g.h" + @class FIAPaymentQueueHandler; @class FIAPReceiptManager; -@interface InAppPurchasePlugin : NSObject +@interface InAppPurchasePlugin : NSObject @property(strong, nonatomic) FIAPaymentQueueHandler *paymentQueueHandler; diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.m index 7086f611acea..88cc4e8a5d03 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.m @@ -40,8 +40,11 @@ + (void)registerWithRegistrar:(NSObject *)registrar { FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase" binaryMessenger:[registrar messenger]]; + InAppPurchasePlugin *instance = [[InAppPurchasePlugin alloc] initWithRegistrar:registrar]; [registrar addMethodCallDelegate:instance channel:channel]; + [registrar addApplicationDelegate:instance]; + SetUpInAppPurchaseAPI(registrar.messenger, instance); } - (instancetype)initWithReceiptManager:(FIAPReceiptManager *)receiptManager { @@ -85,16 +88,8 @@ - (instancetype)initWithRegistrar:(NSObject *)registrar } - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - if ([@"-[SKPaymentQueue canMakePayments:]" isEqualToString:call.method]) { - [self canMakePayments:result]; - } else if ([@"-[SKPaymentQueue transactions]" isEqualToString:call.method]) { - [self getPendingTransactions:result]; - } else if ([@"-[SKPaymentQueue storefront]" isEqualToString:call.method]) { - [self getStorefront:result]; - } else if ([@"-[InAppPurchasePlugin startProductRequest:result:]" isEqualToString:call.method]) { + 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 if ([@"-[InAppPurchasePlugin restoreTransactions:result:]" isEqualToString:call.method]) { @@ -127,34 +122,29 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } } -- (void)canMakePayments:(FlutterResult)result { - result(@([SKPaymentQueue canMakePayments])); +- (nullable NSNumber *)canMakePaymentsWithError: + (FlutterError *_Nullable __autoreleasing *_Nonnull)error { + return @([SKPaymentQueue canMakePayments]); } -- (void)getPendingTransactions:(FlutterResult)result { +- (nullable NSArray *)transactionsWithError: + (FlutterError *_Nullable *_Nonnull)error { NSArray *transactions = [self.paymentQueueHandler getUnfinishedTransactions]; NSMutableArray *transactionMaps = [[NSMutableArray alloc] init]; for (SKPaymentTransaction *transaction in transactions) { - [transactionMaps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]]; + [transactionMaps addObject:[FIAObjectTranslator convertTransactionToPigeon:transaction]]; } - result(transactionMaps); + return transactionMaps; } -- (void)getStorefront:(FlutterResult)result { - if (@available(iOS 13.0, macOS 10.15, *)) { - SKStorefront *storefront = self.paymentQueueHandler.storefront; - if (!storefront) { - result(nil); - return; - } - result([FIAObjectTranslator getMapFromSKStorefront:storefront]); - return; +- (nullable SKStorefrontMessage *)storefrontWithError:(FlutterError *_Nullable *_Nonnull)error + API_AVAILABLE(ios(13.0), macos(10.15)) { + SKStorefront *storefront = self.paymentQueueHandler.storefront; + if (!storefront) { + return nil; } - - NSLog(@"storefront is not avaialbe in iOS below 13.0 or macOS below 10.15."); - result(nil); - return; + return [FIAObjectTranslator convertStorefrontToPigeon:storefront]; } - (void)handleProductRequestMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { @@ -193,28 +183,23 @@ - (void)handleProductRequestMethodCall:(FlutterMethodCall *)call result:(Flutter }]; } -- (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 Dictionary" - details:call.arguments]); - return; - } - NSDictionary *paymentMap = (NSDictionary *)call.arguments; +- (void)addPaymentPaymentMap:(nonnull NSDictionary *)paymentMap + error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { 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 getProduct:productID]; if (!product) { - result([FlutterError + *error = [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." - details:call.arguments]); + details:paymentMap]; return; } + SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product]; payment.applicationUsername = [paymentMap objectForKey:@"applicationUsername"]; NSNumber *quantity = [paymentMap objectForKey:@"quantity"]; @@ -227,34 +212,32 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { if (@available(iOS 12.2, *)) { NSDictionary *paymentDiscountMap = [self getNonNullValueFromDictionary:paymentMap forKey:@"paymentDiscount"]; - NSString *error = nil; + NSString *errorMsg = nil; SKPaymentDiscount *paymentDiscount = - [FIAObjectTranslator getSKPaymentDiscountFromMap:paymentDiscountMap withError:&error]; + [FIAObjectTranslator getSKPaymentDiscountFromMap:paymentDiscountMap withError:&errorMsg]; - if (error) { - result([FlutterError + if (errorMsg) { + *error = [FlutterError errorWithCode:@"storekit_invalid_payment_discount_object" message:[NSString stringWithFormat:@"You have requested a payment and specified a " @"payment discount with invalid properties. %@", - error] - details:call.arguments]); + errorMsg] + details:paymentMap]; return; } payment.paymentDiscount = paymentDiscount; } - if (![self.paymentQueueHandler addPayment:payment]) { - result([FlutterError + *error = [FlutterError errorWithCode:@"storekit_duplicate_product_object" message:@"There is a pending transaction for the same product identifier. Please " @"either wait for it to be finished or finish it manually using " @"`completePurchase` to avoid edge cases." - details:call.arguments]); + details:paymentMap]; return; } - result(nil); } - (void)finishTransaction:(FlutterMethodCall *)call result:(FlutterResult)result { @@ -465,5 +448,4 @@ - (SKProduct *)getProduct:(NSString *)productID { - (SKReceiptRefreshRequest *)getRefreshReceiptRequest:(NSDictionary *)properties { return [[SKReceiptRefreshRequest alloc] initWithReceiptProperties:properties]; } - @end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/messages.g.h b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/messages.g.h new file mode 100644 index 000000000000..3874b7b20a88 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/messages.g.h @@ -0,0 +1,149 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Autogenerated from Pigeon (v16.0.4), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#import + +@protocol FlutterBinaryMessenger; +@protocol FlutterMessageCodec; +@class FlutterError; +@class FlutterStandardTypedData; + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSUInteger, SKPaymentTransactionStateMessage) { + /// Indicates the transaction is being processed in App Store. + /// + /// You should update your UI to indicate that you are waiting for the + /// transaction to update to another state. Never complete a transaction that + /// is still in a purchasing state. + SKPaymentTransactionStateMessagePurchasing = 0, + /// The user's payment has been succesfully processed. + /// + /// You should provide the user the content that they purchased. + SKPaymentTransactionStateMessagePurchased = 1, + /// The transaction failed. + /// + /// Check the [PaymentTransactionWrapper.error] property from + /// [PaymentTransactionWrapper] for details. + SKPaymentTransactionStateMessageFailed = 2, + /// This transaction is restoring content previously purchased by the user. + /// + /// The previous transaction information can be obtained in + /// [PaymentTransactionWrapper.originalTransaction] from + /// [PaymentTransactionWrapper]. + SKPaymentTransactionStateMessageRestored = 3, + /// The transaction is in the queue but pending external action. Wait for + /// another callback to get the final state. + /// + /// You should update your UI to indicate that you are waiting for the + /// transaction to update to another state. + SKPaymentTransactionStateMessageDeferred = 4, + /// Indicates the transaction is in an unspecified state. + SKPaymentTransactionStateMessageUnspecified = 5, +}; + +/// Wrapper for SKPaymentTransactionStateMessage to allow for nullability. +@interface SKPaymentTransactionStateMessageBox : NSObject +@property(nonatomic, assign) SKPaymentTransactionStateMessage value; +- (instancetype)initWithValue:(SKPaymentTransactionStateMessage)value; +@end + +@class SKPaymentTransactionMessage; +@class SKPaymentMessage; +@class SKErrorMessage; +@class SKPaymentDiscountMessage; +@class SKStorefrontMessage; + +@interface SKPaymentTransactionMessage : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithPayment:(SKPaymentMessage *)payment + transactionState:(SKPaymentTransactionStateMessage)transactionState + originalTransaction:(nullable SKPaymentTransactionMessage *)originalTransaction + transactionTimeStamp:(nullable NSNumber *)transactionTimeStamp + transactionIdentifier:(nullable NSString *)transactionIdentifier + error:(nullable SKErrorMessage *)error; +@property(nonatomic, strong) SKPaymentMessage *payment; +@property(nonatomic, assign) SKPaymentTransactionStateMessage transactionState; +@property(nonatomic, strong, nullable) SKPaymentTransactionMessage *originalTransaction; +@property(nonatomic, strong, nullable) NSNumber *transactionTimeStamp; +@property(nonatomic, copy, nullable) NSString *transactionIdentifier; +@property(nonatomic, strong, nullable) SKErrorMessage *error; +@end + +@interface SKPaymentMessage : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithProductIdentifier:(NSString *)productIdentifier + applicationUsername:(nullable NSString *)applicationUsername + requestData:(nullable NSString *)requestData + quantity:(NSInteger)quantity + simulatesAskToBuyInSandbox:(BOOL)simulatesAskToBuyInSandbox + paymentDiscount:(nullable SKPaymentDiscountMessage *)paymentDiscount; +@property(nonatomic, copy) NSString *productIdentifier; +@property(nonatomic, copy, nullable) NSString *applicationUsername; +@property(nonatomic, copy, nullable) NSString *requestData; +@property(nonatomic, assign) NSInteger quantity; +@property(nonatomic, assign) BOOL simulatesAskToBuyInSandbox; +@property(nonatomic, strong, nullable) SKPaymentDiscountMessage *paymentDiscount; +@end + +@interface SKErrorMessage : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithCode:(NSInteger)code + domain:(NSString *)domain + userInfo:(NSDictionary *)userInfo; +@property(nonatomic, assign) NSInteger code; +@property(nonatomic, copy) NSString *domain; +@property(nonatomic, copy) NSDictionary *userInfo; +@end + +@interface SKPaymentDiscountMessage : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithIdentifier:(NSString *)identifier + keyIdentifier:(NSString *)keyIdentifier + nonce:(NSString *)nonce + signature:(NSString *)signature + timestamp:(NSInteger)timestamp; +@property(nonatomic, copy) NSString *identifier; +@property(nonatomic, copy) NSString *keyIdentifier; +@property(nonatomic, copy) NSString *nonce; +@property(nonatomic, copy) NSString *signature; +@property(nonatomic, assign) NSInteger timestamp; +@end + +@interface SKStorefrontMessage : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithCountryCode:(NSString *)countryCode identifier:(NSString *)identifier; +@property(nonatomic, copy) NSString *countryCode; +@property(nonatomic, copy) NSString *identifier; +@end + +/// The codec used by InAppPurchaseAPI. +NSObject *InAppPurchaseAPIGetCodec(void); + +@protocol InAppPurchaseAPI +/// Returns if the current device is able to make payments +/// +/// @return `nil` only when `error != nil`. +- (nullable NSNumber *)canMakePaymentsWithError:(FlutterError *_Nullable *_Nonnull)error; +/// @return `nil` only when `error != nil`. +- (nullable NSArray *)transactionsWithError: + (FlutterError *_Nullable *_Nonnull)error; +/// @return `nil` only when `error != nil`. +- (nullable SKStorefrontMessage *)storefrontWithError:(FlutterError *_Nullable *_Nonnull)error; +- (void)addPaymentPaymentMap:(NSDictionary *)paymentMap + error:(FlutterError *_Nullable *_Nonnull)error; +@end + +extern void SetUpInAppPurchaseAPI(id binaryMessenger, + NSObject *_Nullable api); + +NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/messages.g.m b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/messages.g.m new file mode 100644 index 000000000000..6608b7a0002b --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/messages.g.m @@ -0,0 +1,395 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v16.0.4), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#import "messages.g.h" + +#if TARGET_OS_OSX +#import +#else +#import +#endif + +#if !__has_feature(objc_arc) +#error File requires ARC to be enabled. +#endif + +static NSArray *wrapResult(id result, FlutterError *error) { + if (error) { + return @[ + error.code ?: [NSNull null], error.message ?: [NSNull null], error.details ?: [NSNull null] + ]; + } + return @[ result ?: [NSNull null] ]; +} + +static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { + id result = array[key]; + return (result == [NSNull null]) ? nil : result; +} + +@implementation SKPaymentTransactionStateMessageBox +- (instancetype)initWithValue:(SKPaymentTransactionStateMessage)value { + self = [super init]; + if (self) { + _value = value; + } + return self; +} +@end + +@interface SKPaymentTransactionMessage () ++ (SKPaymentTransactionMessage *)fromList:(NSArray *)list; ++ (nullable SKPaymentTransactionMessage *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@interface SKPaymentMessage () ++ (SKPaymentMessage *)fromList:(NSArray *)list; ++ (nullable SKPaymentMessage *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@interface SKErrorMessage () ++ (SKErrorMessage *)fromList:(NSArray *)list; ++ (nullable SKErrorMessage *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@interface SKPaymentDiscountMessage () ++ (SKPaymentDiscountMessage *)fromList:(NSArray *)list; ++ (nullable SKPaymentDiscountMessage *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@interface SKStorefrontMessage () ++ (SKStorefrontMessage *)fromList:(NSArray *)list; ++ (nullable SKStorefrontMessage *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@implementation SKPaymentTransactionMessage ++ (instancetype)makeWithPayment:(SKPaymentMessage *)payment + transactionState:(SKPaymentTransactionStateMessage)transactionState + originalTransaction:(nullable SKPaymentTransactionMessage *)originalTransaction + transactionTimeStamp:(nullable NSNumber *)transactionTimeStamp + transactionIdentifier:(nullable NSString *)transactionIdentifier + error:(nullable SKErrorMessage *)error { + SKPaymentTransactionMessage *pigeonResult = [[SKPaymentTransactionMessage alloc] init]; + pigeonResult.payment = payment; + pigeonResult.transactionState = transactionState; + pigeonResult.originalTransaction = originalTransaction; + pigeonResult.transactionTimeStamp = transactionTimeStamp; + pigeonResult.transactionIdentifier = transactionIdentifier; + pigeonResult.error = error; + return pigeonResult; +} ++ (SKPaymentTransactionMessage *)fromList:(NSArray *)list { + SKPaymentTransactionMessage *pigeonResult = [[SKPaymentTransactionMessage alloc] init]; + pigeonResult.payment = [SKPaymentMessage nullableFromList:(GetNullableObjectAtIndex(list, 0))]; + pigeonResult.transactionState = [GetNullableObjectAtIndex(list, 1) integerValue]; + pigeonResult.originalTransaction = + [SKPaymentTransactionMessage nullableFromList:(GetNullableObjectAtIndex(list, 2))]; + pigeonResult.transactionTimeStamp = GetNullableObjectAtIndex(list, 3); + pigeonResult.transactionIdentifier = GetNullableObjectAtIndex(list, 4); + pigeonResult.error = [SKErrorMessage nullableFromList:(GetNullableObjectAtIndex(list, 5))]; + return pigeonResult; +} ++ (nullable SKPaymentTransactionMessage *)nullableFromList:(NSArray *)list { + return (list) ? [SKPaymentTransactionMessage fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.payment ? [self.payment toList] : [NSNull null]), + @(self.transactionState), + (self.originalTransaction ? [self.originalTransaction toList] : [NSNull null]), + self.transactionTimeStamp ?: [NSNull null], + self.transactionIdentifier ?: [NSNull null], + (self.error ? [self.error toList] : [NSNull null]), + ]; +} +@end + +@implementation SKPaymentMessage ++ (instancetype)makeWithProductIdentifier:(NSString *)productIdentifier + applicationUsername:(nullable NSString *)applicationUsername + requestData:(nullable NSString *)requestData + quantity:(NSInteger)quantity + simulatesAskToBuyInSandbox:(BOOL)simulatesAskToBuyInSandbox + paymentDiscount:(nullable SKPaymentDiscountMessage *)paymentDiscount { + SKPaymentMessage *pigeonResult = [[SKPaymentMessage alloc] init]; + pigeonResult.productIdentifier = productIdentifier; + pigeonResult.applicationUsername = applicationUsername; + pigeonResult.requestData = requestData; + pigeonResult.quantity = quantity; + pigeonResult.simulatesAskToBuyInSandbox = simulatesAskToBuyInSandbox; + pigeonResult.paymentDiscount = paymentDiscount; + return pigeonResult; +} ++ (SKPaymentMessage *)fromList:(NSArray *)list { + SKPaymentMessage *pigeonResult = [[SKPaymentMessage alloc] init]; + pigeonResult.productIdentifier = GetNullableObjectAtIndex(list, 0); + pigeonResult.applicationUsername = GetNullableObjectAtIndex(list, 1); + pigeonResult.requestData = GetNullableObjectAtIndex(list, 2); + pigeonResult.quantity = [GetNullableObjectAtIndex(list, 3) integerValue]; + pigeonResult.simulatesAskToBuyInSandbox = [GetNullableObjectAtIndex(list, 4) boolValue]; + pigeonResult.paymentDiscount = + [SKPaymentDiscountMessage nullableFromList:(GetNullableObjectAtIndex(list, 5))]; + return pigeonResult; +} ++ (nullable SKPaymentMessage *)nullableFromList:(NSArray *)list { + return (list) ? [SKPaymentMessage fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + self.productIdentifier ?: [NSNull null], + self.applicationUsername ?: [NSNull null], + self.requestData ?: [NSNull null], + @(self.quantity), + @(self.simulatesAskToBuyInSandbox), + (self.paymentDiscount ? [self.paymentDiscount toList] : [NSNull null]), + ]; +} +@end + +@implementation SKErrorMessage ++ (instancetype)makeWithCode:(NSInteger)code + domain:(NSString *)domain + userInfo:(NSDictionary *)userInfo { + SKErrorMessage *pigeonResult = [[SKErrorMessage alloc] init]; + pigeonResult.code = code; + pigeonResult.domain = domain; + pigeonResult.userInfo = userInfo; + return pigeonResult; +} ++ (SKErrorMessage *)fromList:(NSArray *)list { + SKErrorMessage *pigeonResult = [[SKErrorMessage alloc] init]; + pigeonResult.code = [GetNullableObjectAtIndex(list, 0) integerValue]; + pigeonResult.domain = GetNullableObjectAtIndex(list, 1); + pigeonResult.userInfo = GetNullableObjectAtIndex(list, 2); + return pigeonResult; +} ++ (nullable SKErrorMessage *)nullableFromList:(NSArray *)list { + return (list) ? [SKErrorMessage fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + @(self.code), + self.domain ?: [NSNull null], + self.userInfo ?: [NSNull null], + ]; +} +@end + +@implementation SKPaymentDiscountMessage ++ (instancetype)makeWithIdentifier:(NSString *)identifier + keyIdentifier:(NSString *)keyIdentifier + nonce:(NSString *)nonce + signature:(NSString *)signature + timestamp:(NSInteger)timestamp { + SKPaymentDiscountMessage *pigeonResult = [[SKPaymentDiscountMessage alloc] init]; + pigeonResult.identifier = identifier; + pigeonResult.keyIdentifier = keyIdentifier; + pigeonResult.nonce = nonce; + pigeonResult.signature = signature; + pigeonResult.timestamp = timestamp; + return pigeonResult; +} ++ (SKPaymentDiscountMessage *)fromList:(NSArray *)list { + SKPaymentDiscountMessage *pigeonResult = [[SKPaymentDiscountMessage alloc] init]; + pigeonResult.identifier = GetNullableObjectAtIndex(list, 0); + pigeonResult.keyIdentifier = GetNullableObjectAtIndex(list, 1); + pigeonResult.nonce = GetNullableObjectAtIndex(list, 2); + pigeonResult.signature = GetNullableObjectAtIndex(list, 3); + pigeonResult.timestamp = [GetNullableObjectAtIndex(list, 4) integerValue]; + return pigeonResult; +} ++ (nullable SKPaymentDiscountMessage *)nullableFromList:(NSArray *)list { + return (list) ? [SKPaymentDiscountMessage fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + self.identifier ?: [NSNull null], + self.keyIdentifier ?: [NSNull null], + self.nonce ?: [NSNull null], + self.signature ?: [NSNull null], + @(self.timestamp), + ]; +} +@end + +@implementation SKStorefrontMessage ++ (instancetype)makeWithCountryCode:(NSString *)countryCode identifier:(NSString *)identifier { + SKStorefrontMessage *pigeonResult = [[SKStorefrontMessage alloc] init]; + pigeonResult.countryCode = countryCode; + pigeonResult.identifier = identifier; + return pigeonResult; +} ++ (SKStorefrontMessage *)fromList:(NSArray *)list { + SKStorefrontMessage *pigeonResult = [[SKStorefrontMessage alloc] init]; + pigeonResult.countryCode = GetNullableObjectAtIndex(list, 0); + pigeonResult.identifier = GetNullableObjectAtIndex(list, 1); + return pigeonResult; +} ++ (nullable SKStorefrontMessage *)nullableFromList:(NSArray *)list { + return (list) ? [SKStorefrontMessage fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + self.countryCode ?: [NSNull null], + self.identifier ?: [NSNull null], + ]; +} +@end + +@interface InAppPurchaseAPICodecReader : FlutterStandardReader +@end +@implementation InAppPurchaseAPICodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [SKErrorMessage fromList:[self readValue]]; + case 129: + return [SKPaymentDiscountMessage fromList:[self readValue]]; + case 130: + return [SKPaymentMessage fromList:[self readValue]]; + case 131: + return [SKPaymentTransactionMessage fromList:[self readValue]]; + case 132: + return [SKStorefrontMessage fromList:[self readValue]]; + default: + return [super readValueOfType:type]; + } +} +@end + +@interface InAppPurchaseAPICodecWriter : FlutterStandardWriter +@end +@implementation InAppPurchaseAPICodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[SKErrorMessage class]]) { + [self writeByte:128]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[SKPaymentDiscountMessage class]]) { + [self writeByte:129]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[SKPaymentMessage class]]) { + [self writeByte:130]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[SKPaymentTransactionMessage class]]) { + [self writeByte:131]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[SKStorefrontMessage class]]) { + [self writeByte:132]; + [self writeValue:[value toList]]; + } else { + [super writeValue:value]; + } +} +@end + +@interface InAppPurchaseAPICodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation InAppPurchaseAPICodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[InAppPurchaseAPICodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[InAppPurchaseAPICodecReader alloc] initWithData:data]; +} +@end + +NSObject *InAppPurchaseAPIGetCodec(void) { + static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; + dispatch_once(&sPred, ^{ + InAppPurchaseAPICodecReaderWriter *readerWriter = + [[InAppPurchaseAPICodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + +void SetUpInAppPurchaseAPI(id binaryMessenger, + NSObject *api) { + /// Returns if the current device is able to make payments + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName: + @"dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.canMakePayments" + binaryMessenger:binaryMessenger + codec:InAppPurchaseAPIGetCodec()]; + if (api) { + NSCAssert( + [api respondsToSelector:@selector(canMakePaymentsWithError:)], + @"InAppPurchaseAPI api (%@) doesn't respond to @selector(canMakePaymentsWithError:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + NSNumber *output = [api canMakePaymentsWithError:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.transactions" + binaryMessenger:binaryMessenger + codec:InAppPurchaseAPIGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(transactionsWithError:)], + @"InAppPurchaseAPI api (%@) doesn't respond to @selector(transactionsWithError:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + NSArray *output = [api transactionsWithError:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.storefront" + binaryMessenger:binaryMessenger + codec:InAppPurchaseAPIGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(storefrontWithError:)], + @"InAppPurchaseAPI api (%@) doesn't respond to @selector(storefrontWithError:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + SKStorefrontMessage *output = [api storefrontWithError:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.addPayment" + binaryMessenger:binaryMessenger + codec:InAppPurchaseAPIGetCodec()]; + if (api) { + NSCAssert( + [api respondsToSelector:@selector(addPaymentPaymentMap:error:)], + @"InAppPurchaseAPI api (%@) doesn't respond to @selector(addPaymentPaymentMap:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSDictionary *arg_paymentMap = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api addPaymentPaymentMap:arg_paymentMap error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/project.pbxproj b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/project.pbxproj index 1cfaebdf92f4..06e0b3ac947d 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/project.pbxproj @@ -258,7 +258,7 @@ isa = PBXProject; attributes = { DefaultBuildSystemTypeForWorkspace = Original; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -341,12 +341,10 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", "${PODS_CONFIGURATION_BUILD_DIR}/in_app_purchase_storekit/in_app_purchase_storekit_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/shared_preferences_foundation/shared_preferences_foundation_privacy.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/in_app_purchase_storekit_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/shared_preferences_foundation_privacy.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index fc4170f31765..102a865d991a 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ wrapResponse( + {Object? result, PlatformException? error, bool empty = false}) { + if (empty) { + return []; + } + if (error == null) { + return [result]; + } + return [error.code, error.message, error.details]; +} + +enum SKPaymentTransactionStateMessage { + /// Indicates the transaction is being processed in App Store. + /// + /// You should update your UI to indicate that you are waiting for the + /// transaction to update to another state. Never complete a transaction that + /// is still in a purchasing state. + purchasing, + + /// The user's payment has been succesfully processed. + /// + /// You should provide the user the content that they purchased. + purchased, + + /// The transaction failed. + /// + /// Check the [PaymentTransactionWrapper.error] property from + /// [PaymentTransactionWrapper] for details. + failed, + + /// This transaction is restoring content previously purchased by the user. + /// + /// The previous transaction information can be obtained in + /// [PaymentTransactionWrapper.originalTransaction] from + /// [PaymentTransactionWrapper]. + restored, + + /// The transaction is in the queue but pending external action. Wait for + /// another callback to get the final state. + /// + /// You should update your UI to indicate that you are waiting for the + /// transaction to update to another state. + deferred, + + /// Indicates the transaction is in an unspecified state. + unspecified, +} + +class SKPaymentTransactionMessage { + SKPaymentTransactionMessage({ + required this.payment, + required this.transactionState, + this.originalTransaction, + this.transactionTimeStamp, + this.transactionIdentifier, + this.error, + }); + + SKPaymentMessage payment; + + SKPaymentTransactionStateMessage transactionState; + + SKPaymentTransactionMessage? originalTransaction; + + double? transactionTimeStamp; + + String? transactionIdentifier; + + SKErrorMessage? error; + + Object encode() { + return [ + payment.encode(), + transactionState.index, + originalTransaction?.encode(), + transactionTimeStamp, + transactionIdentifier, + error?.encode(), + ]; + } + + static SKPaymentTransactionMessage decode(Object result) { + result as List; + return SKPaymentTransactionMessage( + payment: SKPaymentMessage.decode(result[0]! as List), + transactionState: + SKPaymentTransactionStateMessage.values[result[1]! as int], + originalTransaction: result[2] != null + ? SKPaymentTransactionMessage.decode(result[2]! as List) + : null, + transactionTimeStamp: result[3] as double?, + transactionIdentifier: result[4] as String?, + error: result[5] != null + ? SKErrorMessage.decode(result[5]! as List) + : null, + ); + } +} + +class SKPaymentMessage { + SKPaymentMessage({ + required this.productIdentifier, + this.applicationUsername, + this.requestData, + this.quantity = 1, + this.simulatesAskToBuyInSandbox = false, + this.paymentDiscount, + }); + + String productIdentifier; + + String? applicationUsername; + + String? requestData; + + int quantity; + + bool simulatesAskToBuyInSandbox; + + SKPaymentDiscountMessage? paymentDiscount; + + Object encode() { + return [ + productIdentifier, + applicationUsername, + requestData, + quantity, + simulatesAskToBuyInSandbox, + paymentDiscount?.encode(), + ]; + } + + static SKPaymentMessage decode(Object result) { + result as List; + return SKPaymentMessage( + productIdentifier: result[0]! as String, + applicationUsername: result[1] as String?, + requestData: result[2] as String?, + quantity: result[3]! as int, + simulatesAskToBuyInSandbox: result[4]! as bool, + paymentDiscount: result[5] != null + ? SKPaymentDiscountMessage.decode(result[5]! as List) + : null, + ); + } +} + +class SKErrorMessage { + SKErrorMessage({ + required this.code, + required this.domain, + required this.userInfo, + }); + + int code; + + String domain; + + Map userInfo; + + Object encode() { + return [ + code, + domain, + userInfo, + ]; + } + + static SKErrorMessage decode(Object result) { + result as List; + return SKErrorMessage( + code: result[0]! as int, + domain: result[1]! as String, + userInfo: (result[2] as Map?)!.cast(), + ); + } +} + +class SKPaymentDiscountMessage { + SKPaymentDiscountMessage({ + required this.identifier, + required this.keyIdentifier, + required this.nonce, + required this.signature, + required this.timestamp, + }); + + String identifier; + + String keyIdentifier; + + String nonce; + + String signature; + + int timestamp; + + Object encode() { + return [ + identifier, + keyIdentifier, + nonce, + signature, + timestamp, + ]; + } + + static SKPaymentDiscountMessage decode(Object result) { + result as List; + return SKPaymentDiscountMessage( + identifier: result[0]! as String, + keyIdentifier: result[1]! as String, + nonce: result[2]! as String, + signature: result[3]! as String, + timestamp: result[4]! as int, + ); + } +} + +class SKStorefrontMessage { + SKStorefrontMessage({ + required this.countryCode, + required this.identifier, + }); + + String countryCode; + + String identifier; + + Object encode() { + return [ + countryCode, + identifier, + ]; + } + + static SKStorefrontMessage decode(Object result) { + result as List; + return SKStorefrontMessage( + countryCode: result[0]! as String, + identifier: result[1]! as String, + ); + } +} + +class _InAppPurchaseAPICodec extends StandardMessageCodec { + const _InAppPurchaseAPICodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is SKErrorMessage) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is SKPaymentDiscountMessage) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is SKPaymentMessage) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is SKPaymentTransactionMessage) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else if (value is SKStorefrontMessage) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return SKErrorMessage.decode(readValue(buffer)!); + case 129: + return SKPaymentDiscountMessage.decode(readValue(buffer)!); + case 130: + return SKPaymentMessage.decode(readValue(buffer)!); + case 131: + return SKPaymentTransactionMessage.decode(readValue(buffer)!); + case 132: + return SKStorefrontMessage.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +class InAppPurchaseAPI { + /// Constructor for [InAppPurchaseAPI]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + InAppPurchaseAPI({BinaryMessenger? binaryMessenger}) + : __pigeon_binaryMessenger = binaryMessenger; + final BinaryMessenger? __pigeon_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = + _InAppPurchaseAPICodec(); + + /// Returns if the current device is able to make payments + Future canMakePayments() async { + const String __pigeon_channelName = + 'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.canMakePayments'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = + await __pigeon_channel.send(null) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { + throw PlatformException( + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], + ); + } else if (__pigeon_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (__pigeon_replyList[0] as bool?)!; + } + } + + Future> transactions() async { + const String __pigeon_channelName = + 'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.transactions'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = + await __pigeon_channel.send(null) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { + throw PlatformException( + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], + ); + } else if (__pigeon_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (__pigeon_replyList[0] as List?)! + .cast(); + } + } + + Future storefront() async { + const String __pigeon_channelName = + 'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.storefront'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = + await __pigeon_channel.send(null) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { + throw PlatformException( + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], + ); + } else if (__pigeon_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (__pigeon_replyList[0] as SKStorefrontMessage?)!; + } + } + + Future addPayment(Map paymentMap) async { + const String __pigeon_channelName = + 'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.addPayment'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = + await __pigeon_channel.send([paymentMap]) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { + throw PlatformException( + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], + ); + } else { + return; + } + } +} diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index eace938c574b..5c2e3c395a35 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -12,9 +12,18 @@ import 'package:json_annotation/json_annotation.dart'; import '../../store_kit_wrappers.dart'; import '../channel.dart'; import '../in_app_purchase_storekit_platform.dart'; +import '../messages.g.dart'; part 'sk_payment_queue_wrapper.g.dart'; +InAppPurchaseAPI _hostApi = InAppPurchaseAPI(); + +/// Set up pigeon API. +@visibleForTesting +void setInAppPurchaseHostApi(InAppPurchaseAPI api) { + _hostApi = api; +} + /// A wrapper around /// [`SKPaymentQueue`](https://developer.apple.com/documentation/storekit/skpaymentqueue?language=objc). /// @@ -24,7 +33,7 @@ part 'sk_payment_queue_wrapper.g.dart'; /// /// Full information on using `SKPaymentQueue` and processing purchases is /// available at the [In-App Purchase Programming -/// Guide](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Introduction.html#//apple_ref/doc/uid/TP40008267). +/// Guide](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Introduction.html#//apple_ref/doc/uid/TP40008267) class SKPaymentQueueWrapper { /// Returns the default payment queue. /// @@ -45,25 +54,21 @@ class SKPaymentQueueWrapper { /// /// Returns `null` if the user's device is below iOS 13.0 or macOS 10.15. Future storefront() async { - final Map? storefrontMap = await channel - .invokeMapMethod('-[SKPaymentQueue storefront]'); - if (storefrontMap == null) { - return null; - } - return SKStorefrontWrapper.fromJson(storefrontMap); + return SKStorefrontWrapper.convertFromPigeon(await _hostApi.storefront()); } /// Calls [`-[SKPaymentQueue transactions]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506026-transactions?language=objc). Future> transactions() async { - return _getTransactionList((await channel - .invokeListMethod('-[SKPaymentQueue transactions]'))!); + final List pigeonMsgs = + await _hostApi.transactions(); + return pigeonMsgs + .map((SKPaymentTransactionMessage? msg) => + SKPaymentTransactionWrapper.convertFromPigeon(msg!)) + .toList(); } /// Calls [`-[SKPaymentQueue canMakePayments:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506139-canmakepayments?language=objc). - static Future canMakePayments() async => - (await channel - .invokeMethod('-[SKPaymentQueue canMakePayments:]')) ?? - false; + static Future canMakePayments() async => _hostApi.canMakePayments(); /// Sets an observer to listen to all incoming transaction events. /// @@ -138,11 +143,8 @@ class SKPaymentQueueWrapper { Future addPayment(SKPaymentWrapper payment) async { assert(_observer != null, '[in_app_purchase]: Trying to add a payment without an observer. One must be set using `SkPaymentQueueWrapper.setTransactionObserver` before the app launches.'); - final Map requestMap = payment.toMap(); - await channel.invokeMethod( - '-[InAppPurchasePlugin addPayment:result:]', - requestMap, - ); + + await _hostApi.addPayment(payment.toMap()); } /// Finishes a transaction and removes it from the queue. @@ -358,7 +360,7 @@ class SKError { /// /// Any key of the map must be a valid [NSErrorUserInfoKey](https://developer.apple.com/documentation/foundation/nserroruserinfokey?language=objc). @JsonKey(defaultValue: {}) - final Map userInfo; + final Map userInfo; @override bool operator ==(Object other) { @@ -381,6 +383,11 @@ class SKError { domain, userInfo, ); + + /// Converts [SKErrorMessage] into the dart equivalent + static SKError convertFromPigeon(SKErrorMessage msg) { + return SKError(code: msg.code, domain: msg.domain, userInfo: msg.userInfo); + } } /// Dart wrapper around StoreKit's @@ -498,6 +505,18 @@ class SKPaymentWrapper { @override String toString() => _$SKPaymentWrapperToJson(this).toString(); + + /// Converts [SKPaymentMessage] into the dart equivalent + static SKPaymentWrapper convertFromPigeon(SKPaymentMessage msg) { + return SKPaymentWrapper( + productIdentifier: msg.productIdentifier, + applicationUsername: msg.applicationUsername, + quantity: msg.quantity, + simulatesAskToBuyInSandbox: msg.simulatesAskToBuyInSandbox, + requestData: msg.requestData, + paymentDiscount: + SKPaymentDiscountWrapper.convertFromPigeon(msg.paymentDiscount)); + } } /// Dart wrapper around StoreKit's @@ -596,4 +615,18 @@ class SKPaymentDiscountWrapper { @override int get hashCode => Object.hash(identifier, keyIdentifier, nonce, signature, timestamp); + + /// Converts [SKPaymentDiscountMessage] into the dart equivalent + static SKPaymentDiscountWrapper? convertFromPigeon( + SKPaymentDiscountMessage? msg) { + if (msg == null) { + return null; + } + return SKPaymentDiscountWrapper( + identifier: msg.identifier, + keyIdentifier: msg.keyIdentifier, + nonce: msg.nonce, + signature: msg.signature, + timestamp: msg.timestamp); + } } diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart index 3894721a1f80..7f16e1492a41 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; +import '../messages.g.dart'; import 'enum_converters.dart'; import 'sk_payment_queue_wrapper.dart'; import 'sk_product_wrapper.dart'; @@ -90,7 +91,26 @@ enum SKPaymentTransactionStateWrapper { /// Indicates the transaction is in an unspecified state. @JsonValue(-1) - unspecified, + unspecified; + + /// Converts [SKPaymentTransactionStateMessages] into the dart equivalent + static SKPaymentTransactionStateWrapper convertFromPigeon( + SKPaymentTransactionStateMessage msg) { + switch (msg) { + case SKPaymentTransactionStateMessage.purchased: + return SKPaymentTransactionStateWrapper.purchased; + case SKPaymentTransactionStateMessage.purchasing: + return SKPaymentTransactionStateWrapper.purchasing; + case SKPaymentTransactionStateMessage.failed: + return SKPaymentTransactionStateWrapper.failed; + case SKPaymentTransactionStateMessage.restored: + return SKPaymentTransactionStateWrapper.restored; + case SKPaymentTransactionStateMessage.deferred: + return SKPaymentTransactionStateWrapper.deferred; + case SKPaymentTransactionStateMessage.unspecified: + return SKPaymentTransactionStateWrapper.unspecified; + } + } } /// Created when a payment is added to the [SKPaymentQueueWrapper]. @@ -199,4 +219,20 @@ class SKPaymentTransactionWrapper { 'transactionIdentifier': transactionIdentifier, 'productIdentifier': payment.productIdentifier, }; + + /// Converts [SKPaymentTransactionMessages] into the dart equivalent + static SKPaymentTransactionWrapper convertFromPigeon( + SKPaymentTransactionMessage msg) { + return SKPaymentTransactionWrapper( + payment: SKPaymentWrapper.convertFromPigeon(msg.payment), + transactionState: SKPaymentTransactionStateWrapper.convertFromPigeon( + msg.transactionState), + originalTransaction: msg.originalTransaction == null + ? null + : convertFromPigeon(msg.originalTransaction!), + transactionTimeStamp: msg.transactionTimeStamp, + transactionIdentifier: msg.transactionIdentifier, + error: + msg.error == null ? null : SKError.convertFromPigeon(msg.error!)); + } } diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart index ff9e9b7db746..2ba25e08616a 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart @@ -5,6 +5,8 @@ import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; +import '../messages.g.dart'; + part 'sk_storefront_wrapper.g.dart'; /// Contains the location and unique identifier of an Apple App Store storefront. @@ -65,4 +67,10 @@ class SKStorefrontWrapper { /// Converts the instance to a key value map which can be used to serialize /// to JSON format. Map toMap() => _$SKStorefrontWrapperToJson(this); + + /// Converts the pigeon equivalent to an instance of SKStorefrontWrapper + static SKStorefrontWrapper convertFromPigeon(SKStorefrontMessage msg) { + return SKStorefrontWrapper( + countryCode: msg.countryCode, identifier: msg.identifier); + } } diff --git a/packages/in_app_purchase/in_app_purchase_storekit/pigeons/copyright.txt b/packages/in_app_purchase/in_app_purchase_storekit/pigeons/copyright.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/in_app_purchase/in_app_purchase_storekit/pigeons/messages.dart b/packages/in_app_purchase/in_app_purchase_storekit/pigeons/messages.dart new file mode 100644 index 000000000000..d9d43cd7eff2 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/pigeons/messages.dart @@ -0,0 +1,143 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon(PigeonOptions( + dartOut: 'lib/src/messages.g.dart', + dartTestOut: 'test/test_api.g.dart', + objcHeaderOut: 'darwin/Classes/messages.g.h', + objcSourceOut: 'darwin/Classes/messages.g.m', + copyrightHeader: 'pigeons/copyright.txt', +)) +class SKPaymentTransactionMessage { + SKPaymentTransactionMessage({ + required this.payment, + required this.transactionState, + this.originalTransaction, + this.transactionTimeStamp, + this.transactionIdentifier, + this.error, + }); + + final SKPaymentMessage payment; + + final SKPaymentTransactionStateMessage transactionState; + + final SKPaymentTransactionMessage? originalTransaction; + + final double? transactionTimeStamp; + + final String? transactionIdentifier; + + final SKErrorMessage? error; +} + +enum SKPaymentTransactionStateMessage { + /// Indicates the transaction is being processed in App Store. + /// + /// You should update your UI to indicate that you are waiting for the + /// transaction to update to another state. Never complete a transaction that + /// is still in a purchasing state. + purchasing, + + /// The user's payment has been succesfully processed. + /// + /// You should provide the user the content that they purchased. + purchased, + + /// The transaction failed. + /// + /// Check the [PaymentTransactionWrapper.error] property from + /// [PaymentTransactionWrapper] for details. + failed, + + /// This transaction is restoring content previously purchased by the user. + /// + /// The previous transaction information can be obtained in + /// [PaymentTransactionWrapper.originalTransaction] from + /// [PaymentTransactionWrapper]. + restored, + + /// The transaction is in the queue but pending external action. Wait for + /// another callback to get the final state. + /// + /// You should update your UI to indicate that you are waiting for the + /// transaction to update to another state. + deferred, + + /// Indicates the transaction is in an unspecified state. + unspecified, +} + +class SKPaymentMessage { + /// Creates a new [SKPaymentWrapper] with the provided information. + const SKPaymentMessage({ + required this.productIdentifier, + this.applicationUsername, + this.requestData, + this.quantity = 1, + this.simulatesAskToBuyInSandbox = false, + this.paymentDiscount, + }); + + final String productIdentifier; + + final String? applicationUsername; + + final String? requestData; + + final int quantity; + + final bool simulatesAskToBuyInSandbox; + + final SKPaymentDiscountMessage? paymentDiscount; +} + +class SKErrorMessage { + const SKErrorMessage( + {required this.code, required this.domain, required this.userInfo}); + + final int code; + final String domain; + final Map userInfo; +} + +class SKPaymentDiscountMessage { + const SKPaymentDiscountMessage({ + required this.identifier, + required this.keyIdentifier, + required this.nonce, + required this.signature, + required this.timestamp, + }); + + final String identifier; + final String keyIdentifier; + final String nonce; + final String signature; + final int timestamp; +} + +class SKStorefrontMessage { + const SKStorefrontMessage({ + required this.countryCode, + required this.identifier, + }); + + final String countryCode; + final String identifier; +} + +@HostApi(dartHostTestHandler: 'TestInAppPurchaseApi') +abstract class InAppPurchaseAPI { + /// Returns if the current device is able to make payments + bool canMakePayments(); + + List transactions(); + + SKStorefrontMessage storefront(); + + void addPayment(Map paymentMap); +} diff --git a/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml index af2d7fc48f51..fc7fde8d6220 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_storekit description: An implementation for the iOS and macOS platforms of the Flutter `in_app_purchase` plugin. This uses the StoreKit Framework. repository: https://github.com/flutter/packages/tree/main/packages/in_app_purchase/in_app_purchase_storekit issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.3.8+1 +version: 0.3.9 environment: sdk: ^3.2.3 @@ -31,6 +31,7 @@ dev_dependencies: flutter_test: sdk: flutter json_serializable: ^6.0.0 + pigeon: ^16.0.4 test: ^1.16.0 topics: diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart index ef6eef42535c..d26840ad8c07 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart @@ -8,11 +8,13 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; import 'package:in_app_purchase_storekit/src/channel.dart'; +import 'package:in_app_purchase_storekit/src/messages.g.dart'; import 'package:in_app_purchase_storekit/store_kit_wrappers.dart'; import '../store_kit_wrappers/sk_test_stub_objects.dart'; +import '../test_api.g.dart'; -class FakeStoreKitPlatform { +class FakeStoreKitPlatform implements TestInAppPurchaseApi { FakeStoreKitPlatform() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, onMethodCall); @@ -22,7 +24,7 @@ class FakeStoreKitPlatform { String? receiptData; late Set validProductIDs; late Map validProducts; - late List transactions; + late List transactionList; late List finishedTransactions; late bool testRestoredTransactionsNull; late bool testTransactionFail; @@ -34,7 +36,7 @@ class FakeStoreKitPlatform { Map discountReceived = {}; void reset() { - transactions = []; + transactionList = []; receiptData = 'dummy base64data'; validProductIDs = {'123', '456'}; validProducts = {}; @@ -122,8 +124,6 @@ class FakeStoreKitPlatform { Future onMethodCall(MethodCall call) { switch (call.method) { - case '-[SKPaymentQueue canMakePayments:]': - return Future.value(true); case '-[InAppPurchasePlugin startProductRequest:result:]': if (queryProductException != null) { throw queryProductException!; @@ -154,7 +154,7 @@ class FakeStoreKitPlatform { } if (!testRestoredTransactionsNull) { InAppPurchaseStoreKitPlatform.observer - .updatedTransactions(transactions: transactions); + .updatedTransactions(transactions: transactionList); } InAppPurchaseStoreKitPlatform.observer .paymentQueueRestoreCompletedTransactionsFinished(); @@ -187,7 +187,7 @@ class FakeStoreKitPlatform { final SKPaymentTransactionWrapper transaction = createPendingTransaction(id, quantity: quantity); - transactions.add(transaction); + transactionList.add(transaction); InAppPurchaseStoreKitPlatform.observer.updatedTransactions( transactions: [transaction]); sleep(const Duration(milliseconds: 30)); @@ -215,7 +215,7 @@ class FakeStoreKitPlatform { finishedTransactions.add(createPurchasedTransaction( arguments['productIdentifier']! as String, arguments['transactionIdentifier']! as String, - quantity: transactions.first.payment.quantity)); + quantity: transactionList.first.payment.quantity)); case '-[SKPaymentQueue startObservingTransactionQueue]': queueIsActive = true; case '-[SKPaymentQueue stopObservingTransactionQueue]': @@ -231,4 +231,61 @@ class FakeStoreKitPlatform { Map _getArgumentDictionary(MethodCall call) { return (call.arguments as Map).cast(); } + + @override + bool canMakePayments() { + return true; + } + + @override + void addPayment(Map paymentMap) { + final String id = paymentMap['productIdentifier']! as String; + final int quantity = paymentMap['quantity']! as int; + + // Keep the received paymentDiscount parameter when testing payment with discount. + if (paymentMap['applicationUsername']! == 'userWithDiscount') { + final Map? discountArgument = + paymentMap['paymentDiscount'] as Map?; + if (discountArgument != null) { + discountReceived = discountArgument.cast(); + } else { + discountReceived = {}; + } + } + + final SKPaymentTransactionWrapper transaction = + createPendingTransaction(id, quantity: quantity); + transactionList.add(transaction); + InAppPurchaseStoreKitPlatform.observer.updatedTransactions( + transactions: [transaction]); + if (testTransactionFail) { + final SKPaymentTransactionWrapper transactionFailed = + createFailedTransaction(id, quantity: quantity); + InAppPurchaseStoreKitPlatform.observer.updatedTransactions( + transactions: [transactionFailed]); + } else if (testTransactionCancel > 0) { + final SKPaymentTransactionWrapper transactionCanceled = + createCanceledTransaction(id, testTransactionCancel, + quantity: quantity); + InAppPurchaseStoreKitPlatform.observer.updatedTransactions( + transactions: [transactionCanceled]); + } else { + final SKPaymentTransactionWrapper transactionFinished = + createPurchasedTransaction( + id, transaction.transactionIdentifier ?? '', + quantity: quantity); + InAppPurchaseStoreKitPlatform.observer.updatedTransactions( + transactions: [transactionFinished]); + } + } + + @override + SKStorefrontMessage storefront() { + throw UnimplementedError(); + } + + @override + List transactions() { + throw UnimplementedError(); + } } diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_addtion_test.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_addtion_test.dart index 444d7b6ddc09..99fd49d03a32 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_addtion_test.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_addtion_test.dart @@ -8,6 +8,7 @@ import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_inte import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart'; import 'fakes/fake_storekit_platform.dart'; +import 'test_api.g.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -15,6 +16,7 @@ void main() { final FakeStoreKitPlatform fakeStoreKitPlatform = FakeStoreKitPlatform(); setUpAll(() { + TestInAppPurchaseApi.setup(fakeStoreKitPlatform); TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler( SystemChannels.platform, fakeStoreKitPlatform.onMethodCall); diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart index 7ce3161e3f05..278a69184468 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart @@ -13,6 +13,7 @@ import 'package:in_app_purchase_storekit/store_kit_wrappers.dart'; import 'fakes/fake_storekit_platform.dart'; import 'store_kit_wrappers/sk_test_stub_objects.dart'; +import 'test_api.g.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -21,6 +22,7 @@ void main() { late InAppPurchaseStoreKitPlatform iapStoreKitPlatform; setUpAll(() { + TestInAppPurchaseApi.setup(fakeStoreKitPlatform); TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler( SystemChannels.platform, fakeStoreKitPlatform.onMethodCall); @@ -79,9 +81,9 @@ void main() { group('restore purchases', () { test('should emit restored transactions on purchase stream', () async { - fakeStoreKitPlatform.transactions.insert( + fakeStoreKitPlatform.transactionList.insert( 0, fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT1')); - fakeStoreKitPlatform.transactions.insert( + fakeStoreKitPlatform.transactionList.insert( 1, fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT2')); final Completer> completer = Completer>(); @@ -100,9 +102,9 @@ void main() { final List details = await completer.future; expect(details.length, 2); - for (int i = 0; i < fakeStoreKitPlatform.transactions.length; i++) { + for (int i = 0; i < fakeStoreKitPlatform.transactionList.length; i++) { final SKPaymentTransactionWrapper expected = - fakeStoreKitPlatform.transactions[i]; + fakeStoreKitPlatform.transactionList[i]; final PurchaseDetails actual = details[i]; expect(actual.purchaseID, expected.transactionIdentifier); @@ -137,11 +139,11 @@ void main() { }); test('should not block transaction updates', () async { - fakeStoreKitPlatform.transactions.insert( + fakeStoreKitPlatform.transactionList.insert( 0, fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT1')); - fakeStoreKitPlatform.transactions.insert( + fakeStoreKitPlatform.transactionList.insert( 1, fakeStoreKitPlatform.createPurchasedTransaction('foo', 'bar')); - fakeStoreKitPlatform.transactions.insert( + fakeStoreKitPlatform.transactionList.insert( 2, fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT2')); final Completer> completer = Completer>(); @@ -158,9 +160,9 @@ void main() { await iapStoreKitPlatform.restorePurchases(); final List details = await completer.future; expect(details.length, 3); - for (int i = 0; i < fakeStoreKitPlatform.transactions.length; i++) { + for (int i = 0; i < fakeStoreKitPlatform.transactionList.length; i++) { final SKPaymentTransactionWrapper expected = - fakeStoreKitPlatform.transactions[i]; + fakeStoreKitPlatform.transactionList[i]; final PurchaseDetails actual = details[i]; expect(actual.purchaseID, expected.transactionIdentifier); @@ -181,7 +183,7 @@ void main() { test( 'should emit empty transaction if transactions array does not contain a transaction with PurchaseStatus.restored status.', () async { - fakeStoreKitPlatform.transactions.insert( + fakeStoreKitPlatform.transactionList.insert( 0, fakeStoreKitPlatform.createPurchasedTransaction('foo', 'bar')); final Completer>> completer = Completer>>(); @@ -203,9 +205,9 @@ void main() { final List> details = await completer.future; expect(details.length, 2); expect(details[0], >[]); - for (int i = 0; i < fakeStoreKitPlatform.transactions.length; i++) { + for (int i = 0; i < fakeStoreKitPlatform.transactionList.length; i++) { final SKPaymentTransactionWrapper expected = - fakeStoreKitPlatform.transactions[i]; + fakeStoreKitPlatform.transactionList[i]; final PurchaseDetails actual = details[1][i]; expect(actual.purchaseID, expected.transactionIdentifier); @@ -225,9 +227,9 @@ void main() { test('receipt error should populate null to verificationData.data', () async { - fakeStoreKitPlatform.transactions.insert( + fakeStoreKitPlatform.transactionList.insert( 0, fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT1')); - fakeStoreKitPlatform.transactions.insert( + fakeStoreKitPlatform.transactionList.insert( 1, fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT2')); fakeStoreKitPlatform.receiptData = null; final Completer> completer = diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_methodchannel_apis_test.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_methodchannel_apis_test.dart index de6aa3765299..44228ebb523b 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_methodchannel_apis_test.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_methodchannel_apis_test.dart @@ -5,7 +5,9 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:in_app_purchase_storekit/src/channel.dart'; +import 'package:in_app_purchase_storekit/src/messages.g.dart'; import 'package:in_app_purchase_storekit/store_kit_wrappers.dart'; +import '../test_api.g.dart'; import 'sk_test_stub_objects.dart'; void main() { @@ -14,6 +16,7 @@ void main() { final FakeStoreKitPlatform fakeStoreKitPlatform = FakeStoreKitPlatform(); setUpAll(() { + TestInAppPurchaseApi.setup(fakeStoreKitPlatform); TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler( SystemChannels.platform, fakeStoreKitPlatform.onMethodCall); @@ -101,12 +104,6 @@ void main() { expect(await SKPaymentQueueWrapper.canMakePayments(), true); }); - test('canMakePayment returns false if method channel returns null', - () async { - fakeStoreKitPlatform.testReturnNull = true; - expect(await SKPaymentQueueWrapper.canMakePayments(), false); - }); - test('storefront returns valid SKStoreFrontWrapper object', () async { final SKPaymentQueueWrapper queue = SKPaymentQueueWrapper(); expect( @@ -117,12 +114,6 @@ void main() { })); }); - test('storefront returns null', () async { - fakeStoreKitPlatform.testReturnNull = true; - final SKPaymentQueueWrapper queue = SKPaymentQueueWrapper(); - expect(await queue.storefront(), isNull); - }); - test('transactions should return a valid list of transactions', () async { expect(await SKPaymentQueueWrapper().transactions(), isNotEmpty); }); @@ -200,7 +191,7 @@ void main() { }); } -class FakeStoreKitPlatform { +class FakeStoreKitPlatform implements TestInAppPurchaseApi { FakeStoreKitPlatform() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, onMethodCall); @@ -255,27 +246,6 @@ class FakeStoreKitPlatform { throw Exception('some arbitrary error'); } return Future.value('receipt data'); - // payment queue - case '-[SKPaymentQueue canMakePayments:]': - if (testReturnNull) { - return Future.value(); - } - return Future.value(true); - case '-[SKPaymentQueue transactions]': - return Future>.value( - [buildTransactionMap(dummyTransaction)]); - case '-[SKPaymentQueue storefront]': - if (testReturnNull) { - return Future.value(); - } - return Future>.value(const { - 'countryCode': 'USA', - 'identifier': 'unique_identifier', - }); - case '-[InAppPurchasePlugin addPayment:result:]': - payments.add(SKPaymentWrapper.fromJson(Map.from( - call.arguments as Map))); - return Future.sync(() {}); case '-[InAppPurchasePlugin finishTransaction:result:]': transactionsFinished.add( Map.from(call.arguments as Map)); @@ -304,6 +274,27 @@ class FakeStoreKitPlatform { } return Future.error('method not mocked'); } + + @override + void addPayment(Map paymentMap) { + payments + .add(SKPaymentWrapper.fromJson(Map.from(paymentMap))); + } + + @override + bool canMakePayments() { + return true; + } + + @override + SKStorefrontMessage storefront() { + return SKStorefrontMessage( + countryCode: 'USA', identifier: 'unique_identifier'); + } + + @override + List transactions() => + [dummyTransactionMessage]; } class TestPaymentQueueDelegate extends SKPaymentQueueDelegateWrapper {} diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_test_stub_objects.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_test_stub_objects.dart index 6601a21c4ee4..ad32a0a5c14a 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_test_stub_objects.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_test_stub_objects.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:in_app_purchase_storekit/src/messages.g.dart'; import 'package:in_app_purchase_storekit/store_kit_wrappers.dart'; const SKPaymentWrapper dummyPayment = SKPaymentWrapper( @@ -11,6 +12,13 @@ const SKPaymentWrapper dummyPayment = SKPaymentWrapper( quantity: 2, simulatesAskToBuyInSandbox: true); +SKPaymentMessage dummyPaymentMessage = SKPaymentMessage( + productIdentifier: 'prod-id', + applicationUsername: 'app-user-name', + requestData: 'fake-data-utf8', + quantity: 2, + simulatesAskToBuyInSandbox: true); + final SKPaymentWrapper dummyPaymentWithDiscount = SKPaymentWrapper( productIdentifier: 'prod-id', applicationUsername: 'app-user-name', @@ -43,6 +51,11 @@ final SKPaymentTransactionWrapper dummyTransaction = error: dummyError, ); +final SKPaymentTransactionMessage dummyTransactionMessage = + SKPaymentTransactionMessage( + payment: dummyPaymentMessage, + transactionState: SKPaymentTransactionStateMessage.purchased); + final SKPriceLocaleWrapper dollarLocale = SKPriceLocaleWrapper( currencySymbol: r'$', currencyCode: 'USD', @@ -196,6 +209,22 @@ Map buildTransactionMap( return map; } +Map buildTransactionMessage( + SKPaymentTransactionWrapper transaction) { + final Map map = { + 'transactionState': SKPaymentTransactionStateWrapper.values + .indexOf(SKPaymentTransactionStateWrapper.purchased), + 'payment': transaction.payment.toMap(), + 'originalTransaction': transaction.originalTransaction == null + ? null + : buildTransactionMap(transaction.originalTransaction!), + 'transactionTimeStamp': transaction.transactionTimeStamp, + 'transactionIdentifier': transaction.transactionIdentifier, + 'error': buildErrorMap(transaction.error!), + }; + return map; +} + final SKPaymentDiscountWrapper dummyPaymentDiscountWrapper = SKPaymentDiscountWrapper.fromJson(const { 'identifier': 'dummy-discount-identifier', diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/test_api.g.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/test_api.g.dart new file mode 100644 index 000000000000..a640a188c43c --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/test_api.g.dart @@ -0,0 +1,182 @@ +// Autogenerated from Pigeon (v16.0.4), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import, no_leading_underscores_for_local_identifiers +// ignore_for_file: avoid_relative_lib_imports +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:in_app_purchase_storekit/src/messages.g.dart'; + +class _TestInAppPurchaseApiCodec extends StandardMessageCodec { + const _TestInAppPurchaseApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is SKErrorMessage) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is SKPaymentDiscountMessage) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is SKPaymentMessage) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is SKPaymentTransactionMessage) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else if (value is SKStorefrontMessage) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return SKErrorMessage.decode(readValue(buffer)!); + case 129: + return SKPaymentDiscountMessage.decode(readValue(buffer)!); + case 130: + return SKPaymentMessage.decode(readValue(buffer)!); + case 131: + return SKPaymentTransactionMessage.decode(readValue(buffer)!); + case 132: + return SKStorefrontMessage.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestInAppPurchaseApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; + static const MessageCodec pigeonChannelCodec = + _TestInAppPurchaseApiCodec(); + + /// Returns if the current device is able to make payments + bool canMakePayments(); + + List transactions(); + + SKStorefrontMessage storefront(); + + void addPayment(Map paymentMap); + + static void setup(TestInAppPurchaseApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel __pigeon_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.canMakePayments', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(__pigeon_channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(__pigeon_channel, + (Object? message) async { + try { + final bool output = api.canMakePayments(); + return [output]; + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel __pigeon_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.transactions', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(__pigeon_channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(__pigeon_channel, + (Object? message) async { + try { + final List output = + api.transactions(); + return [output]; + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel __pigeon_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.storefront', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(__pigeon_channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(__pigeon_channel, + (Object? message) async { + try { + final SKStorefrontMessage output = api.storefront(); + return [output]; + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel __pigeon_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.addPayment', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(__pigeon_channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(__pigeon_channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.addPayment was null.'); + final List args = (message as List?)!; + final Map? arg_paymentMap = + (args[0] as Map?)?.cast(); + assert(arg_paymentMap != null, + 'Argument for dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.addPayment was null, expected non-null Map.'); + try { + api.addPayment(arg_paymentMap!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + } +}