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

Commit 45f87b9

Browse files
authored
[in_app_purchase] Add support for promotional offers through Store-Kit wrappers (#4458)
1 parent 8d0382e commit 45f87b9

File tree

12 files changed

+500
-9
lines changed

12 files changed

+500
-9
lines changed

packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ - (void)testAddPaymentFailure {
111111
XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStateFailed);
112112
}
113113

114-
- (void)testAddPaymentSuccessWithMockQueue {
114+
- (void)testAddPaymentSuccessWithoutPaymentDiscount {
115115
XCTestExpectation* expectation =
116116
[self expectationWithDescription:@"result should return success state"];
117117
FlutterMethodCall* call =
@@ -129,6 +129,9 @@ - (void)testAddPaymentSuccessWithMockQueue {
129129
SKPaymentTransaction* transaction = transactions[0];
130130
if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
131131
transactionForUpdateBlock = transaction;
132+
if (@available(iOS 12.2, *)) {
133+
XCTAssertNil(transaction.payment.paymentDiscount);
134+
}
132135
[expectation fulfill];
133136
}
134137
}
@@ -147,6 +150,93 @@ - (void)testAddPaymentSuccessWithMockQueue {
147150
XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased);
148151
}
149152

153+
- (void)testAddPaymentSuccessWithPaymentDiscount {
154+
XCTestExpectation* expectation =
155+
[self expectationWithDescription:@"result should return success state"];
156+
FlutterMethodCall* call =
157+
[FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]"
158+
arguments:@{
159+
@"productIdentifier" : @"123",
160+
@"quantity" : @(1),
161+
@"simulatesAskToBuyInSandbox" : @YES,
162+
@"paymentDiscount" : @{
163+
@"identifier" : @"test_identifier",
164+
@"keyIdentifier" : @"test_key_identifier",
165+
@"nonce" : @"4a11a9cc-3bc3-11ec-8d3d-0242ac130003",
166+
@"signature" : @"test_signature",
167+
@"timestamp" : @(1635847102),
168+
}
169+
}];
170+
SKPaymentQueueStub* queue = [SKPaymentQueueStub new];
171+
queue.testState = SKPaymentTransactionStatePurchased;
172+
__block SKPaymentTransaction* transactionForUpdateBlock;
173+
self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue
174+
transactionsUpdated:^(NSArray<SKPaymentTransaction*>* _Nonnull transactions) {
175+
SKPaymentTransaction* transaction = transactions[0];
176+
if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
177+
transactionForUpdateBlock = transaction;
178+
if (@available(iOS 12.2, *)) {
179+
SKPaymentDiscount* paymentDiscount = transaction.payment.paymentDiscount;
180+
XCTAssertEqual(paymentDiscount.identifier, @"test_identifier");
181+
XCTAssertEqual(paymentDiscount.keyIdentifier, @"test_key_identifier");
182+
XCTAssertEqualObjects(
183+
paymentDiscount.nonce,
184+
[[NSUUID alloc] initWithUUIDString:@"4a11a9cc-3bc3-11ec-8d3d-0242ac130003"]);
185+
XCTAssertEqual(paymentDiscount.signature, @"test_signature");
186+
XCTAssertEqual(paymentDiscount.timestamp, @(1635847102));
187+
}
188+
[expectation fulfill];
189+
}
190+
}
191+
transactionRemoved:nil
192+
restoreTransactionFailed:nil
193+
restoreCompletedTransactionsFinished:nil
194+
shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) {
195+
return YES;
196+
}
197+
updatedDownloads:nil];
198+
[queue addTransactionObserver:self.plugin.paymentQueueHandler];
199+
[self.plugin handleMethodCall:call
200+
result:^(id r){
201+
}];
202+
[self waitForExpectations:@[ expectation ] timeout:5];
203+
XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased);
204+
}
205+
206+
- (void)testAddPaymentFailureWithInvalidPaymentDiscount {
207+
XCTestExpectation* expectation =
208+
[self expectationWithDescription:@"result should return success state"];
209+
FlutterMethodCall* call =
210+
[FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]"
211+
arguments:@{
212+
@"productIdentifier" : @"123",
213+
@"quantity" : @(1),
214+
@"simulatesAskToBuyInSandbox" : @YES,
215+
@"paymentDiscount" : @{
216+
@"keyIdentifier" : @"test_key_identifier",
217+
@"nonce" : @"4a11a9cc-3bc3-11ec-8d3d-0242ac130003",
218+
@"signature" : @"test_signature",
219+
@"timestamp" : @(1635847102),
220+
}
221+
}];
222+
223+
[self.plugin
224+
handleMethodCall:call
225+
result:^(id r) {
226+
XCTAssertTrue([r isKindOfClass:FlutterError.class]);
227+
FlutterError* result = r;
228+
XCTAssertEqualObjects(result.code, @"storekit_invalid_payment_discount_object");
229+
XCTAssertEqualObjects(result.message,
230+
@"You have requested a payment and specified a payment "
231+
@"discount with invalid properties. When specifying a "
232+
@"payment discount the 'identifier' field is mandatory.");
233+
XCTAssertEqualObjects(result.details, call.arguments);
234+
[expectation fulfill];
235+
}];
236+
237+
[self waitForExpectations:@[ expectation ] timeout:5];
238+
}
239+
150240
- (void)testAddPaymentWithNullSandboxArgument {
151241
XCTestExpectation* expectation =
152242
[self expectationWithDescription:@"result should return success state"];

packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ - (instancetype)initWithMap:(NSDictionary *)map {
6161
[self setValue:map[@"subscriptionGroupIdentifier"] ?: [NSNull null]
6262
forKey:@"subscriptionGroupIdentifier"];
6363
}
64+
if (@available(iOS 12.2, *)) {
65+
NSMutableArray *discounts = [[NSMutableArray alloc] init];
66+
for (NSDictionary *discountMap in map[@"discounts"]) {
67+
[discounts addObject:[[SKProductDiscountStub alloc] initWithMap:discountMap]];
68+
}
69+
70+
[self setValue:discounts forKey:@"discounts"];
71+
}
6472
}
6573
return self;
6674
}

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

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ @interface TranslatorTest : XCTestCase
1414
@property(strong, nonatomic) NSMutableDictionary *productMap;
1515
@property(strong, nonatomic) NSDictionary *productResponseMap;
1616
@property(strong, nonatomic) NSDictionary *paymentMap;
17+
@property(copy, nonatomic) NSDictionary *paymentDiscountMap;
1718
@property(strong, nonatomic) NSDictionary *transactionMap;
1819
@property(strong, nonatomic) NSDictionary *errorMap;
1920
@property(strong, nonatomic) NSDictionary *localeMap;
@@ -45,6 +46,9 @@ - (void)setUp {
4546
self.productMap[@"subscriptionPeriod"] = self.periodMap;
4647
self.productMap[@"introductoryPrice"] = self.discountMap;
4748
}
49+
if (@available(iOS 12.2, *)) {
50+
self.productMap[@"discounts"] = @[ self.discountMap ];
51+
}
4852

4953
if (@available(iOS 12.0, *)) {
5054
self.productMap[@"subscriptionGroupIdentifier"] = @"com.group";
@@ -59,6 +63,13 @@ - (void)setUp {
5963
@"applicationUsername" : @"app user name",
6064
@"simulatesAskToBuyInSandbox" : @(NO)
6165
};
66+
self.paymentDiscountMap = @{
67+
@"identifier" : @"payment_discount_identifier",
68+
@"keyIdentifier" : @"payment_discount_key_identifier",
69+
@"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52",
70+
@"signature" : @"this is a encrypted signature",
71+
@"timestamp" : @([NSDate date].timeIntervalSince1970),
72+
};
6273
NSDictionary *originalTransactionMap = @{
6374
@"transactionIdentifier" : @"567",
6475
@"transactionState" : @(SKPaymentTransactionStatePurchasing),
@@ -175,4 +186,134 @@ - (void)testSKStorefrontAndSKPaymentTransactionToMap {
175186
}
176187
}
177188

189+
- (void)testSKPaymentDiscountFromMap {
190+
if (@available(iOS 12.2, *)) {
191+
NSString *error = nil;
192+
SKPaymentDiscount *paymentDiscount =
193+
[FIAObjectTranslator getSKPaymentDiscountFromMap:self.paymentDiscountMap withError:&error];
194+
195+
XCTAssertEqual(paymentDiscount.identifier, self.paymentDiscountMap[@"identifier"]);
196+
XCTAssertEqual(paymentDiscount.keyIdentifier, self.paymentDiscountMap[@"keyIdentifier"]);
197+
XCTAssertEqualObjects(paymentDiscount.nonce,
198+
[[NSUUID alloc] initWithUUIDString:self.paymentDiscountMap[@"nonce"]]);
199+
XCTAssertEqual(paymentDiscount.signature, self.paymentDiscountMap[@"signature"]);
200+
XCTAssertEqual(paymentDiscount.timestamp, self.paymentDiscountMap[@"timestamp"]);
201+
}
202+
}
203+
204+
- (void)testSKPaymentDiscountFromMapMissingIdentifier {
205+
if (@available(iOS 12.2, *)) {
206+
NSArray *invalidValues = @[ [NSNull null], @(1), @"" ];
207+
208+
for (id value in invalidValues) {
209+
NSDictionary *discountMap = @{
210+
@"identifier" : value,
211+
@"keyIdentifier" : @"payment_discount_key_identifier",
212+
@"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52",
213+
@"signature" : @"this is a encrypted signature",
214+
@"timestamp" : @([NSDate date].timeIntervalSince1970),
215+
};
216+
217+
NSString *error = nil;
218+
[FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error];
219+
220+
XCTAssertNotNil(error);
221+
XCTAssertEqualObjects(
222+
error, @"When specifying a payment discount the 'identifier' field is mandatory.");
223+
}
224+
}
225+
}
226+
227+
- (void)testSKPaymentDiscountFromMapMissingKeyIdentifier {
228+
if (@available(iOS 12.2, *)) {
229+
NSArray *invalidValues = @[ [NSNull null], @(1), @"" ];
230+
231+
for (id value in invalidValues) {
232+
NSDictionary *discountMap = @{
233+
@"identifier" : @"payment_discount_identifier",
234+
@"keyIdentifier" : value,
235+
@"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52",
236+
@"signature" : @"this is a encrypted signature",
237+
@"timestamp" : @([NSDate date].timeIntervalSince1970),
238+
};
239+
240+
NSString *error = nil;
241+
[FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error];
242+
243+
XCTAssertNotNil(error);
244+
XCTAssertEqualObjects(
245+
error, @"When specifying a payment discount the 'keyIdentifier' field is mandatory.");
246+
}
247+
}
248+
}
249+
250+
- (void)testSKPaymentDiscountFromMapMissingNonce {
251+
if (@available(iOS 12.2, *)) {
252+
NSArray *invalidValues = @[ [NSNull null], @(1), @"" ];
253+
254+
for (id value in invalidValues) {
255+
NSDictionary *discountMap = @{
256+
@"identifier" : @"payment_discount_identifier",
257+
@"keyIdentifier" : @"payment_discount_key_identifier",
258+
@"nonce" : value,
259+
@"signature" : @"this is a encrypted signature",
260+
@"timestamp" : @([NSDate date].timeIntervalSince1970),
261+
};
262+
263+
NSString *error = nil;
264+
[FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error];
265+
266+
XCTAssertNotNil(error);
267+
XCTAssertEqualObjects(error,
268+
@"When specifying a payment discount the 'nonce' field is mandatory.");
269+
}
270+
}
271+
}
272+
273+
- (void)testSKPaymentDiscountFromMapMissingSignature {
274+
if (@available(iOS 12.2, *)) {
275+
NSArray *invalidValues = @[ [NSNull null], @(1), @"" ];
276+
277+
for (id value in invalidValues) {
278+
NSDictionary *discountMap = @{
279+
@"identifier" : @"payment_discount_identifier",
280+
@"keyIdentifier" : @"payment_discount_key_identifier",
281+
@"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52",
282+
@"signature" : value,
283+
@"timestamp" : @([NSDate date].timeIntervalSince1970),
284+
};
285+
286+
NSString *error = nil;
287+
[FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error];
288+
289+
XCTAssertNotNil(error);
290+
XCTAssertEqualObjects(
291+
error, @"When specifying a payment discount the 'signature' field is mandatory.");
292+
}
293+
}
294+
}
295+
296+
- (void)testSKPaymentDiscountFromMapMissingTimestamp {
297+
if (@available(iOS 12.2, *)) {
298+
NSArray *invalidValues = @[ [NSNull null], @"", @(-1) ];
299+
300+
for (id value in invalidValues) {
301+
NSDictionary *discountMap = @{
302+
@"identifier" : @"payment_discount_identifier",
303+
@"keyIdentifier" : @"payment_discount_key_identifier",
304+
@"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52",
305+
@"signature" : @"this is a encrypted signature",
306+
@"timestamp" : value,
307+
};
308+
309+
NSString *error = nil;
310+
[FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error];
311+
312+
XCTAssertNotNil(error);
313+
XCTAssertEqualObjects(
314+
error, @"When specifying a payment discount the 'timestamp' field is mandatory.");
315+
}
316+
}
317+
}
318+
178319
@end

packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ NS_ASSUME_NONNULL_BEGIN
2020
+ (NSDictionary *)getMapFromSKProductDiscount:(SKProductDiscount *)discount
2121
API_AVAILABLE(ios(11.2));
2222

23+
// Converts an array of SKProductDiscount instances into an array of dictionaries.
24+
+ (nonnull NSArray *)getMapArrayFromSKProductDiscounts:
25+
(nonnull NSArray<SKProductDiscount *> *)productDiscounts API_AVAILABLE(ios(12.2));
26+
2327
// Converts an instance of SKProductsResponse into a dictionary.
2428
+ (NSDictionary *)getMapFromSKProductsResponse:(SKProductsResponse *)productResponse;
2529

@@ -47,6 +51,11 @@ NS_ASSUME_NONNULL_BEGIN
4751
andSKPaymentTransaction:(SKPaymentTransaction *)transaction
4852
API_AVAILABLE(ios(13), macos(10.15), watchos(6.2));
4953

54+
// Creates an instance of the SKPaymentDiscount class based on the supplied dictionary.
55+
+ (nullable SKPaymentDiscount *)getSKPaymentDiscountFromMap:(NSDictionary *)map
56+
withError:(NSString *_Nullable *_Nullable)error
57+
API_AVAILABLE(ios(12.2));
58+
5059
@end
5160
;
5261

0 commit comments

Comments
 (0)