Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 84f5ec6

Browse files
authored
[in_app_purchase][iOS] fix iOS promotional offers (SKPaymentDiscountWrapper was not used properly) (#6541)
1 parent f55c7ff commit 84f5ec6

File tree

12 files changed

+128
-7
lines changed

12 files changed

+128
-7
lines changed

packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.3.3
2+
3+
* Supports adding discount information to AppStorePurchaseParam.
4+
* Fixes iOS Promotional Offers bug which prevents them from working.
5+
16
## 0.3.2+2
27

38
* Updates imports for `prefer_relative_imports`.

packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/TranslatorTests.m

+23
Original file line numberDiff line numberDiff line change
@@ -390,4 +390,27 @@ - (void)testSKPaymentDiscountFromMapMissingTimestamp {
390390
}
391391
}
392392

393+
- (void)testSKPaymentDiscountFromMapOverflowingTimestamp {
394+
if (@available(iOS 12.2, *)) {
395+
NSDictionary *discountMap = @{
396+
@"identifier" : @"payment_discount_identifier",
397+
@"keyIdentifier" : @"payment_discount_key_identifier",
398+
@"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52",
399+
@"signature" : @"this is a encrypted signature",
400+
@"timestamp" : @1665044583595, // timestamp 2022 Oct
401+
};
402+
NSString *error = nil;
403+
SKPaymentDiscount *paymentDiscount =
404+
[FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error];
405+
XCTAssertNil(error);
406+
XCTAssertNotNil(paymentDiscount);
407+
XCTAssertEqual(paymentDiscount.identifier, discountMap[@"identifier"]);
408+
XCTAssertEqual(paymentDiscount.keyIdentifier, discountMap[@"keyIdentifier"]);
409+
XCTAssertEqualObjects(paymentDiscount.nonce,
410+
[[NSUUID alloc] initWithUUIDString:discountMap[@"nonce"]]);
411+
XCTAssertEqual(paymentDiscount.signature, discountMap[@"signature"]);
412+
XCTAssertEqual(paymentDiscount.timestamp, discountMap[@"timestamp"]);
413+
}
414+
}
415+
393416
@end

packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.m

+1-1
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ + (SKPaymentDiscount *)getSKPaymentDiscountFromMap:(NSDictionary *)map
277277
return nil;
278278
}
279279

280-
if (!timestamp || ![timestamp isKindOfClass:NSNumber.class] || [timestamp intValue] <= 0) {
280+
if (!timestamp || ![timestamp isKindOfClass:NSNumber.class] || [timestamp longLongValue] <= 0) {
281281
if (error) {
282282
*error = @"When specifying a payment discount the 'timestamp' field is mandatory.";
283283
}

packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.m

+9-3
Original file line numberDiff line numberDiff line change
@@ -200,10 +200,11 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result {
200200
: [simulatesAskToBuyInSandbox boolValue];
201201

202202
if (@available(iOS 12.2, *)) {
203+
NSDictionary *paymentDiscountMap = [self getNonNullValueFromDictionary:paymentMap
204+
forKey:@"paymentDiscount"];
203205
NSString *error = nil;
204-
SKPaymentDiscount *paymentDiscount = [FIAObjectTranslator
205-
getSKPaymentDiscountFromMap:[paymentMap objectForKey:@"paymentDiscount"]
206-
withError:&error];
206+
SKPaymentDiscount *paymentDiscount =
207+
[FIAObjectTranslator getSKPaymentDiscountFromMap:paymentDiscountMap withError:&error];
207208

208209
if (error) {
209210
result([FlutterError
@@ -367,6 +368,11 @@ - (void)showPriceConsentIfNeeded:(FlutterResult)result {
367368
result(nil);
368369
}
369370

371+
- (id)getNonNullValueFromDictionary:(NSDictionary *)dictionary forKey:(NSString *)key {
372+
id value = dictionary[key];
373+
return [value isKindOfClass:[NSNull class]] ? nil : value;
374+
}
375+
370376
#pragma mark - transaction observer:
371377

372378
- (void)handleTransactionsUpdated:(NSArray<SKPaymentTransaction *> *)transactions {

packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart

+4-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,10 @@ class InAppPurchaseStoreKitPlatform extends InAppPurchasePlatform {
7575
purchaseParam is AppStorePurchaseParam ? purchaseParam.quantity : 1,
7676
applicationUsername: purchaseParam.applicationUserName,
7777
simulatesAskToBuyInSandbox: purchaseParam is AppStorePurchaseParam &&
78-
purchaseParam.simulatesAskToBuyInSandbox));
78+
purchaseParam.simulatesAskToBuyInSandbox,
79+
paymentDiscount: purchaseParam is AppStorePurchaseParam
80+
? purchaseParam.discount
81+
: null));
7982

8083
return true; // There's no error feedback from iOS here to return.
8184
}

packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,8 @@ class SKPaymentWrapper {
405405
'applicationUsername': applicationUsername,
406406
'requestData': requestData,
407407
'quantity': quantity,
408-
'simulatesAskToBuyInSandbox': simulatesAskToBuyInSandbox
408+
'simulatesAskToBuyInSandbox': simulatesAskToBuyInSandbox,
409+
'paymentDiscount': paymentDiscount?.toMap(),
409410
};
410411
}
411412

packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_param.dart

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class AppStorePurchaseParam extends PurchaseParam {
1414
String? applicationUserName,
1515
this.quantity = 1,
1616
this.simulatesAskToBuyInSandbox = false,
17+
this.discount,
1718
}) : super(
1819
productDetails: productDetails,
1920
applicationUserName: applicationUserName,
@@ -32,4 +33,7 @@ class AppStorePurchaseParam extends PurchaseParam {
3233

3334
/// Quantity of the product user requested to buy.
3435
final int quantity;
36+
37+
/// Discount applied to the product. The value is `null` when the product does not have a discount.
38+
final SKPaymentDiscountWrapper? discount;
3539
}

packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: in_app_purchase_storekit
22
description: An implementation for the iOS platform of the Flutter `in_app_purchase` plugin. This uses the StoreKit Framework.
33
repository: https://github.com/flutter/plugins/tree/main/packages/in_app_purchase/in_app_purchase_storekit
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22
5-
version: 0.3.2+2
5+
version: 0.3.3
66

77
environment:
88
sdk: ">=2.14.0 <3.0.0"

packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart

+14
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class FakeStoreKitPlatform {
3030
PlatformException? restoreException;
3131
SKError? testRestoredError;
3232
bool queueIsActive = false;
33+
Map<String, dynamic> discountReceived = <String, dynamic>{};
3334

3435
void reset() {
3536
transactions = <SKPaymentTransactionWrapper>[];
@@ -54,6 +55,7 @@ class FakeStoreKitPlatform {
5455
restoreException = null;
5556
testRestoredError = null;
5657
queueIsActive = false;
58+
discountReceived = <String, dynamic>{};
5759
}
5860

5961
SKPaymentTransactionWrapper createPendingTransaction(String id,
@@ -169,6 +171,18 @@ class FakeStoreKitPlatform {
169171
case '-[InAppPurchasePlugin addPayment:result:]':
170172
final String id = call.arguments['productIdentifier'] as String;
171173
final int quantity = call.arguments['quantity'] as int;
174+
175+
// Keep the received paymentDiscount parameter when testing payment with discount.
176+
if (call.arguments['applicationUsername'] == 'userWithDiscount') {
177+
if (call.arguments['paymentDiscount'] != null) {
178+
final Map<dynamic, dynamic> discountArgument =
179+
call.arguments['paymentDiscount'];
180+
discountReceived = discountArgument.cast<String, dynamic>();
181+
} else {
182+
discountReceived = <String, dynamic>{};
183+
}
184+
}
185+
172186
final SKPaymentTransactionWrapper transaction =
173187
createPendingTransaction(id, quantity: quantity);
174188
transactions.add(transaction);

packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart

+32
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,38 @@ void main() {
489489
expect(
490490
fakeStoreKitPlatform.finishedTransactions.first.payment.quantity, 5);
491491
});
492+
493+
test(
494+
'buying non consumable with discount, should get purchase objects in the purchase update callback',
495+
() async {
496+
final List<PurchaseDetails> details = <PurchaseDetails>[];
497+
final Completer<List<PurchaseDetails>> completer =
498+
Completer<List<PurchaseDetails>>();
499+
final Stream<List<PurchaseDetails>> stream =
500+
iapStoreKitPlatform.purchaseStream;
501+
502+
late StreamSubscription<List<PurchaseDetails>> subscription;
503+
subscription = stream.listen((List<PurchaseDetails> purchaseDetailsList) {
504+
details.addAll(purchaseDetailsList);
505+
if (purchaseDetailsList.first.status == PurchaseStatus.purchased) {
506+
completer.complete(details);
507+
subscription.cancel();
508+
}
509+
});
510+
final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam(
511+
productDetails:
512+
AppStoreProductDetails.fromSKProduct(dummyProductWrapper),
513+
applicationUserName: 'userWithDiscount',
514+
discount: dummyPaymentDiscountWrapper,
515+
);
516+
await iapStoreKitPlatform.buyNonConsumable(purchaseParam: purchaseParam);
517+
518+
final List<PurchaseDetails> result = await completer.future;
519+
expect(result.length, 2);
520+
expect(result.first.productID, dummyProductWrapper.productIdentifier);
521+
expect(fakeStoreKitPlatform.discountReceived,
522+
dummyPaymentDiscountWrapper.toMap());
523+
});
492524
});
493525

494526
group('complete purchase', () {

packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_product_test.dart

+15
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,21 @@ void main() {
141141
expect(payment, equals(dummyPayment));
142142
});
143143

144+
test('SKPaymentWrapper should have propery values consistent with .toMap()',
145+
() {
146+
final Map<String, dynamic> mapResult = dummyPaymentWithDiscount.toMap();
147+
expect(mapResult['productIdentifier'],
148+
dummyPaymentWithDiscount.productIdentifier);
149+
expect(mapResult['applicationUsername'],
150+
dummyPaymentWithDiscount.applicationUsername);
151+
expect(mapResult['requestData'], dummyPaymentWithDiscount.requestData);
152+
expect(mapResult['quantity'], dummyPaymentWithDiscount.quantity);
153+
expect(mapResult['simulatesAskToBuyInSandbox'],
154+
dummyPaymentWithDiscount.simulatesAskToBuyInSandbox);
155+
expect(mapResult['paymentDiscount'],
156+
equals(dummyPaymentWithDiscount.paymentDiscount?.toMap()));
157+
});
158+
144159
test('Should construct correct SKError from json', () {
145160
final SKError error = SKError.fromJson(buildErrorMap(dummyError));
146161
expect(error, equals(dummyError));

packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_test_stub_objects.dart

+18
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ const SKPaymentWrapper dummyPayment = SKPaymentWrapper(
1010
requestData: 'fake-data-utf8',
1111
quantity: 2,
1212
simulatesAskToBuyInSandbox: true);
13+
14+
final SKPaymentWrapper dummyPaymentWithDiscount = SKPaymentWrapper(
15+
productIdentifier: 'prod-id',
16+
applicationUsername: 'app-user-name',
17+
requestData: 'fake-data-utf8',
18+
quantity: 2,
19+
simulatesAskToBuyInSandbox: true,
20+
paymentDiscount: dummyPaymentDiscountWrapper);
21+
1322
const SKError dummyError = SKError(
1423
code: 111,
1524
domain: 'dummy-domain',
@@ -186,3 +195,12 @@ Map<String, dynamic> buildTransactionMap(
186195
};
187196
return map;
188197
}
198+
199+
final SKPaymentDiscountWrapper dummyPaymentDiscountWrapper =
200+
SKPaymentDiscountWrapper.fromJson(const <String, dynamic>{
201+
'identifier': 'dummy-discount-identifier',
202+
'keyIdentifier': 'KEYIDTEST1',
203+
'nonce': '00000000-0000-0000-0000-000000000000',
204+
'signature': 'dummy-signature-string',
205+
'timestamp': 1231231231,
206+
});

0 commit comments

Comments
 (0)