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 52f59efacd6a..324e0608b7f9 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.3.3 + +* Supports adding discount information to AppStorePurchaseParam. +* Fixes iOS Promotional Offers bug which prevents them from working. + ## 0.3.2+2 * Updates imports for `prefer_relative_imports`. diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/TranslatorTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/TranslatorTests.m index ed302d61d9b0..34d686753762 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/TranslatorTests.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/TranslatorTests.m @@ -390,4 +390,27 @@ - (void)testSKPaymentDiscountFromMapMissingTimestamp { } } +- (void)testSKPaymentDiscountFromMapOverflowingTimestamp { + if (@available(iOS 12.2, *)) { + NSDictionary *discountMap = @{ + @"identifier" : @"payment_discount_identifier", + @"keyIdentifier" : @"payment_discount_key_identifier", + @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52", + @"signature" : @"this is a encrypted signature", + @"timestamp" : @1665044583595, // timestamp 2022 Oct + }; + NSString *error = nil; + SKPaymentDiscount *paymentDiscount = + [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error]; + XCTAssertNil(error); + XCTAssertNotNil(paymentDiscount); + XCTAssertEqual(paymentDiscount.identifier, discountMap[@"identifier"]); + XCTAssertEqual(paymentDiscount.keyIdentifier, discountMap[@"keyIdentifier"]); + XCTAssertEqualObjects(paymentDiscount.nonce, + [[NSUUID alloc] initWithUUIDString:discountMap[@"nonce"]]); + XCTAssertEqual(paymentDiscount.signature, discountMap[@"signature"]); + XCTAssertEqual(paymentDiscount.timestamp, discountMap[@"timestamp"]); + } +} + @end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.m index d01eb9becf3d..c656b58808b3 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.m @@ -277,7 +277,7 @@ + (SKPaymentDiscount *)getSKPaymentDiscountFromMap:(NSDictionary *)map return nil; } - if (!timestamp || ![timestamp isKindOfClass:NSNumber.class] || [timestamp intValue] <= 0) { + if (!timestamp || ![timestamp isKindOfClass:NSNumber.class] || [timestamp longLongValue] <= 0) { if (error) { *error = @"When specifying a payment discount the 'timestamp' field is mandatory."; } diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.m index d64c24563b62..bfc90ea43716 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.m @@ -200,10 +200,11 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { : [simulatesAskToBuyInSandbox boolValue]; if (@available(iOS 12.2, *)) { + NSDictionary *paymentDiscountMap = [self getNonNullValueFromDictionary:paymentMap + forKey:@"paymentDiscount"]; NSString *error = nil; - SKPaymentDiscount *paymentDiscount = [FIAObjectTranslator - getSKPaymentDiscountFromMap:[paymentMap objectForKey:@"paymentDiscount"] - withError:&error]; + SKPaymentDiscount *paymentDiscount = + [FIAObjectTranslator getSKPaymentDiscountFromMap:paymentDiscountMap withError:&error]; if (error) { result([FlutterError @@ -367,6 +368,11 @@ - (void)showPriceConsentIfNeeded:(FlutterResult)result { result(nil); } +- (id)getNonNullValueFromDictionary:(NSDictionary *)dictionary forKey:(NSString *)key { + id value = dictionary[key]; + return [value isKindOfClass:[NSNull class]] ? nil : value; +} + #pragma mark - transaction observer: - (void)handleTransactionsUpdated:(NSArray *)transactions { diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart index c03f15f8ce48..0e5e420ece85 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart @@ -75,7 +75,10 @@ class InAppPurchaseStoreKitPlatform extends InAppPurchasePlatform { purchaseParam is AppStorePurchaseParam ? purchaseParam.quantity : 1, applicationUsername: purchaseParam.applicationUserName, simulatesAskToBuyInSandbox: purchaseParam is AppStorePurchaseParam && - purchaseParam.simulatesAskToBuyInSandbox)); + purchaseParam.simulatesAskToBuyInSandbox, + paymentDiscount: purchaseParam is AppStorePurchaseParam + ? purchaseParam.discount + : null)); return true; // There's no error feedback from iOS here to 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 78e16e22416c..d360a2da3fe5 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 @@ -405,7 +405,8 @@ class SKPaymentWrapper { 'applicationUsername': applicationUsername, 'requestData': requestData, 'quantity': quantity, - 'simulatesAskToBuyInSandbox': simulatesAskToBuyInSandbox + 'simulatesAskToBuyInSandbox': simulatesAskToBuyInSandbox, + 'paymentDiscount': paymentDiscount?.toMap(), }; } diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_param.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_param.dart index 168ef5cea5f4..0e7e24166c4d 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_param.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_param.dart @@ -14,6 +14,7 @@ class AppStorePurchaseParam extends PurchaseParam { String? applicationUserName, this.quantity = 1, this.simulatesAskToBuyInSandbox = false, + this.discount, }) : super( productDetails: productDetails, applicationUserName: applicationUserName, @@ -32,4 +33,7 @@ class AppStorePurchaseParam extends PurchaseParam { /// Quantity of the product user requested to buy. final int quantity; + + /// Discount applied to the product. The value is `null` when the product does not have a discount. + final SKPaymentDiscountWrapper? discount; } 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 f2193e53b591..0b6e21a26978 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 platform of the Flutter `in_app_purchase` plugin. This uses the StoreKit Framework. repository: https://github.com/flutter/plugins/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.2+2 +version: 0.3.3 environment: sdk: ">=2.14.0 <3.0.0" 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 08b9c85961a3..e64876deccba 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 @@ -30,6 +30,7 @@ class FakeStoreKitPlatform { PlatformException? restoreException; SKError? testRestoredError; bool queueIsActive = false; + Map discountReceived = {}; void reset() { transactions = []; @@ -54,6 +55,7 @@ class FakeStoreKitPlatform { restoreException = null; testRestoredError = null; queueIsActive = false; + discountReceived = {}; } SKPaymentTransactionWrapper createPendingTransaction(String id, @@ -169,6 +171,18 @@ class FakeStoreKitPlatform { case '-[InAppPurchasePlugin addPayment:result:]': final String id = call.arguments['productIdentifier'] as String; final int quantity = call.arguments['quantity'] as int; + + // Keep the received paymentDiscount parameter when testing payment with discount. + if (call.arguments['applicationUsername'] == 'userWithDiscount') { + if (call.arguments['paymentDiscount'] != null) { + final Map discountArgument = + call.arguments['paymentDiscount']; + discountReceived = discountArgument.cast(); + } else { + discountReceived = {}; + } + } + final SKPaymentTransactionWrapper transaction = createPendingTransaction(id, quantity: quantity); transactions.add(transaction); 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 852599ac3670..51ff2c229483 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 @@ -489,6 +489,38 @@ void main() { expect( fakeStoreKitPlatform.finishedTransactions.first.payment.quantity, 5); }); + + test( + 'buying non consumable with discount, should get purchase objects in the purchase update callback', + () async { + final List details = []; + final Completer> completer = + Completer>(); + final Stream> stream = + iapStoreKitPlatform.purchaseStream; + + late StreamSubscription> subscription; + subscription = stream.listen((List purchaseDetailsList) { + details.addAll(purchaseDetailsList); + if (purchaseDetailsList.first.status == PurchaseStatus.purchased) { + completer.complete(details); + subscription.cancel(); + } + }); + final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( + productDetails: + AppStoreProductDetails.fromSKProduct(dummyProductWrapper), + applicationUserName: 'userWithDiscount', + discount: dummyPaymentDiscountWrapper, + ); + await iapStoreKitPlatform.buyNonConsumable(purchaseParam: purchaseParam); + + final List result = await completer.future; + expect(result.length, 2); + expect(result.first.productID, dummyProductWrapper.productIdentifier); + expect(fakeStoreKitPlatform.discountReceived, + dummyPaymentDiscountWrapper.toMap()); + }); }); group('complete purchase', () { diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_product_test.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_product_test.dart index de61268e4009..b6de5e035c5e 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_product_test.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_product_test.dart @@ -141,6 +141,21 @@ void main() { expect(payment, equals(dummyPayment)); }); + test('SKPaymentWrapper should have propery values consistent with .toMap()', + () { + final Map mapResult = dummyPaymentWithDiscount.toMap(); + expect(mapResult['productIdentifier'], + dummyPaymentWithDiscount.productIdentifier); + expect(mapResult['applicationUsername'], + dummyPaymentWithDiscount.applicationUsername); + expect(mapResult['requestData'], dummyPaymentWithDiscount.requestData); + expect(mapResult['quantity'], dummyPaymentWithDiscount.quantity); + expect(mapResult['simulatesAskToBuyInSandbox'], + dummyPaymentWithDiscount.simulatesAskToBuyInSandbox); + expect(mapResult['paymentDiscount'], + equals(dummyPaymentWithDiscount.paymentDiscount?.toMap())); + }); + test('Should construct correct SKError from json', () { final SKError error = SKError.fromJson(buildErrorMap(dummyError)); expect(error, equals(dummyError)); 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 946fbc81b74c..6601a21c4ee4 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 @@ -10,6 +10,15 @@ const SKPaymentWrapper dummyPayment = SKPaymentWrapper( requestData: 'fake-data-utf8', quantity: 2, simulatesAskToBuyInSandbox: true); + +final SKPaymentWrapper dummyPaymentWithDiscount = SKPaymentWrapper( + productIdentifier: 'prod-id', + applicationUsername: 'app-user-name', + requestData: 'fake-data-utf8', + quantity: 2, + simulatesAskToBuyInSandbox: true, + paymentDiscount: dummyPaymentDiscountWrapper); + const SKError dummyError = SKError( code: 111, domain: 'dummy-domain', @@ -186,3 +195,12 @@ Map buildTransactionMap( }; return map; } + +final SKPaymentDiscountWrapper dummyPaymentDiscountWrapper = + SKPaymentDiscountWrapper.fromJson(const { + 'identifier': 'dummy-discount-identifier', + 'keyIdentifier': 'KEYIDTEST1', + 'nonce': '00000000-0000-0000-0000-000000000000', + 'signature': 'dummy-signature-string', + 'timestamp': 1231231231, +});