[in_app_purchase][iOS] fix iOS promotional offers (SKPaymentDiscountWrapper was not used properly) (#6541)
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 52f59ef..324e060 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 ed302d6..34d6867 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)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 d01eb9b..c656b58 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 @@
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 d64c245..bfc90ea 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 @@
: [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 @@
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<SKPaymentTransaction *> *)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 c03f15f..0e5e420 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 @@
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 78e16e2..d360a2d 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 @@
'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 168ef5c..0e7e241 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 @@
String? applicationUserName,
this.quantity = 1,
this.simulatesAskToBuyInSandbox = false,
+ this.discount,
}) : super(
productDetails: productDetails,
applicationUserName: applicationUserName,
@@ -32,4 +33,7 @@
/// 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 f2193e5..0b6e21a 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 @@
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 08b9c85..e64876d 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 @@
PlatformException? restoreException;
SKError? testRestoredError;
bool queueIsActive = false;
+ Map<String, dynamic> discountReceived = <String, dynamic>{};
void reset() {
transactions = <SKPaymentTransactionWrapper>[];
@@ -54,6 +55,7 @@
restoreException = null;
testRestoredError = null;
queueIsActive = false;
+ discountReceived = <String, dynamic>{};
}
SKPaymentTransactionWrapper createPendingTransaction(String id,
@@ -169,6 +171,18 @@
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<dynamic, dynamic> discountArgument =
+ call.arguments['paymentDiscount'];
+ discountReceived = discountArgument.cast<String, dynamic>();
+ } else {
+ discountReceived = <String, dynamic>{};
+ }
+ }
+
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 852599a..51ff2c2 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 @@
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<PurchaseDetails> details = <PurchaseDetails>[];
+ final Completer<List<PurchaseDetails>> completer =
+ Completer<List<PurchaseDetails>>();
+ final Stream<List<PurchaseDetails>> stream =
+ iapStoreKitPlatform.purchaseStream;
+
+ late StreamSubscription<List<PurchaseDetails>> subscription;
+ subscription = stream.listen((List<PurchaseDetails> 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<PurchaseDetails> 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 de61268..b6de5e0 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 @@
expect(payment, equals(dummyPayment));
});
+ test('SKPaymentWrapper should have propery values consistent with .toMap()',
+ () {
+ final Map<String, dynamic> 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 946fbc8..6601a21 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 @@
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 @@
};
return map;
}
+
+final SKPaymentDiscountWrapper dummyPaymentDiscountWrapper =
+ SKPaymentDiscountWrapper.fromJson(const <String, dynamic>{
+ 'identifier': 'dummy-discount-identifier',
+ 'keyIdentifier': 'KEYIDTEST1',
+ 'nonce': '00000000-0000-0000-0000-000000000000',
+ 'signature': 'dummy-signature-string',
+ 'timestamp': 1231231231,
+});