mirror of
https://github.com/flutter/packages.git
synced 2025-07-01 23:51:55 +08:00
[in_app_purchase] Convert refreshReceipt(), startObservingPaymentQueue(), stopObservingPaymentQueue(), registerPaymentQueueDelegate(), removePaymentQueueDelegate(), showPriceConsentIfNeeded() to Pigeon (#6165)
Part 3 of https://github.com/flutter/flutter/issues/117910
This commit is contained in:
@ -1,3 +1,8 @@
|
||||
## 0.3.12
|
||||
|
||||
* Converts `refreshReceipt()`, `startObservingPaymentQueue()`, `stopObservingPaymentQueue()`,
|
||||
`registerPaymentQueueDelegate()`, `removePaymentQueueDelegate()`, `showPriceConsentIfNeeded()` to pigeon.
|
||||
|
||||
## 0.3.11
|
||||
|
||||
* Fixes SKError.userInfo not being nullable.
|
||||
|
@ -87,30 +87,6 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
|
||||
if ([@"-[InAppPurchasePlugin retrieveReceiptData:result:]" isEqualToString:call.method]) {
|
||||
[self retrieveReceiptData:call result:result];
|
||||
} else if ([@"-[InAppPurchasePlugin refreshReceipt:result:]" isEqualToString:call.method]) {
|
||||
[self refreshReceipt:call result:result];
|
||||
} else if ([@"-[SKPaymentQueue startObservingTransactionQueue]" isEqualToString:call.method]) {
|
||||
[self startObservingPaymentQueue:result];
|
||||
} else if ([@"-[SKPaymentQueue stopObservingTransactionQueue]" isEqualToString:call.method]) {
|
||||
[self stopObservingPaymentQueue:result];
|
||||
#if TARGET_OS_IOS
|
||||
} else if ([@"-[SKPaymentQueue registerDelegate]" isEqualToString:call.method]) {
|
||||
[self registerPaymentQueueDelegate:result];
|
||||
#endif
|
||||
} else if ([@"-[SKPaymentQueue removeDelegate]" isEqualToString:call.method]) {
|
||||
[self removePaymentQueueDelegate:result];
|
||||
#if TARGET_OS_IOS
|
||||
} else if ([@"-[SKPaymentQueue showPriceConsentIfNeeded]" isEqualToString:call.method]) {
|
||||
[self showPriceConsentIfNeeded:result];
|
||||
#endif
|
||||
} else {
|
||||
result(FlutterMethodNotImplemented);
|
||||
}
|
||||
}
|
||||
|
||||
- (nullable NSNumber *)canMakePaymentsWithError:
|
||||
(FlutterError *_Nullable __autoreleasing *_Nonnull)error {
|
||||
return @([SKPaymentQueue canMakePayments]);
|
||||
@ -270,62 +246,61 @@
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)retrieveReceiptData:(FlutterMethodCall *)call result:(FlutterResult)result {
|
||||
FlutterError *error = nil;
|
||||
NSString *receiptData = [self.receiptManager retrieveReceiptWithError:&error];
|
||||
if (error) {
|
||||
result(error);
|
||||
return;
|
||||
- (nullable NSString *)retrieveReceiptDataWithError:
|
||||
(FlutterError *_Nullable __autoreleasing *_Nonnull)error {
|
||||
FlutterError *flutterError;
|
||||
NSString *receiptData = [self.receiptManager retrieveReceiptWithError:&flutterError];
|
||||
if (flutterError) {
|
||||
*error = flutterError;
|
||||
return nil;
|
||||
}
|
||||
result(receiptData);
|
||||
return receiptData;
|
||||
}
|
||||
|
||||
- (void)refreshReceipt:(FlutterMethodCall *)call result:(FlutterResult)result {
|
||||
NSDictionary *arguments = call.arguments;
|
||||
- (void)refreshReceiptReceiptProperties:(nullable NSDictionary *)receiptProperties
|
||||
completion:(nonnull void (^)(FlutterError *_Nullable))completion {
|
||||
SKReceiptRefreshRequest *request;
|
||||
if (arguments) {
|
||||
if (![arguments isKindOfClass:[NSDictionary class]]) {
|
||||
result([FlutterError errorWithCode:@"storekit_invalid_argument"
|
||||
message:@"Argument type of startRequest is not array"
|
||||
details:call.arguments]);
|
||||
return;
|
||||
}
|
||||
if (receiptProperties) {
|
||||
// if recieptProperties is not null, this call is for testing.
|
||||
NSMutableDictionary *properties = [NSMutableDictionary new];
|
||||
properties[SKReceiptPropertyIsExpired] = arguments[@"isExpired"];
|
||||
properties[SKReceiptPropertyIsRevoked] = arguments[@"isRevoked"];
|
||||
properties[SKReceiptPropertyIsVolumePurchase] = arguments[@"isVolumePurchase"];
|
||||
properties[SKReceiptPropertyIsExpired] = receiptProperties[@"isExpired"];
|
||||
properties[SKReceiptPropertyIsRevoked] = receiptProperties[@"isRevoked"];
|
||||
properties[SKReceiptPropertyIsVolumePurchase] = receiptProperties[@"isVolumePurchase"];
|
||||
request = [self getRefreshReceiptRequest:properties];
|
||||
} else {
|
||||
request = [self getRefreshReceiptRequest:nil];
|
||||
}
|
||||
|
||||
FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request];
|
||||
[self.requestHandlers addObject:handler];
|
||||
__weak typeof(self) weakSelf = self;
|
||||
[handler startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable response,
|
||||
NSError *_Nullable error) {
|
||||
FlutterError *requestError;
|
||||
if (error) {
|
||||
result([FlutterError errorWithCode:@"storekit_refreshreceiptrequest_platform_error"
|
||||
requestError = [FlutterError errorWithCode:@"storekit_refreshreceiptrequest_platform_error"
|
||||
message:error.localizedDescription
|
||||
details:error.description]);
|
||||
return;
|
||||
details:error.description];
|
||||
completion(requestError);
|
||||
}
|
||||
result(nil);
|
||||
completion(nil);
|
||||
[weakSelf.requestHandlers removeObject:handler];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)startObservingPaymentQueue:(FlutterResult)result {
|
||||
- (void)startObservingPaymentQueueWithError:
|
||||
(FlutterError *_Nullable __autoreleasing *_Nonnull)error {
|
||||
[_paymentQueueHandler startObservingPaymentQueue];
|
||||
result(nil);
|
||||
}
|
||||
|
||||
- (void)stopObservingPaymentQueue:(FlutterResult)result {
|
||||
- (void)stopObservingPaymentQueueWithError:
|
||||
(FlutterError *_Nullable __autoreleasing *_Nonnull)error {
|
||||
[_paymentQueueHandler stopObservingPaymentQueue];
|
||||
result(nil);
|
||||
}
|
||||
|
||||
- (void)registerPaymentQueueDelegateWithError:
|
||||
(FlutterError *_Nullable __autoreleasing *_Nonnull)error {
|
||||
#if TARGET_OS_IOS
|
||||
- (void)registerPaymentQueueDelegate:(FlutterResult)result {
|
||||
if (@available(iOS 13.0, *)) {
|
||||
_paymentQueueDelegateCallbackChannel = [FlutterMethodChannel
|
||||
methodChannelWithName:@"plugins.flutter.io/in_app_purchase_payment_queue_delegate"
|
||||
@ -335,27 +310,25 @@
|
||||
initWithMethodChannel:_paymentQueueDelegateCallbackChannel];
|
||||
_paymentQueueHandler.delegate = _paymentQueueDelegate;
|
||||
}
|
||||
result(nil);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)removePaymentQueueDelegate:(FlutterResult)result {
|
||||
- (void)removePaymentQueueDelegateWithError:
|
||||
(FlutterError *_Nullable __autoreleasing *_Nonnull)error {
|
||||
if (@available(iOS 13.0, *)) {
|
||||
_paymentQueueHandler.delegate = nil;
|
||||
}
|
||||
_paymentQueueDelegate = nil;
|
||||
_paymentQueueDelegateCallbackChannel = nil;
|
||||
result(nil);
|
||||
}
|
||||
|
||||
- (void)showPriceConsentIfNeededWithError:(FlutterError *_Nullable __autoreleasing *_Nonnull)error {
|
||||
#if TARGET_OS_IOS
|
||||
- (void)showPriceConsentIfNeeded:(FlutterResult)result {
|
||||
if (@available(iOS 13.4, *)) {
|
||||
[_paymentQueueHandler showPriceConsentIfNeeded];
|
||||
}
|
||||
result(nil);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (id)getNonNullValueFromDictionary:(NSDictionary *)dictionary forKey:(NSString *)key {
|
||||
id value = dictionary[key];
|
||||
|
@ -1,7 +1,6 @@
|
||||
// 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
|
||||
|
||||
@ -271,6 +270,14 @@ NSObject<FlutterMessageCodec> *InAppPurchaseAPIGetCodec(void);
|
||||
- (void)restoreTransactionsApplicationUserName:(nullable NSString *)applicationUserName
|
||||
error:(FlutterError *_Nullable *_Nonnull)error;
|
||||
- (void)presentCodeRedemptionSheetWithError:(FlutterError *_Nullable *_Nonnull)error;
|
||||
- (nullable NSString *)retrieveReceiptDataWithError:(FlutterError *_Nullable *_Nonnull)error;
|
||||
- (void)refreshReceiptReceiptProperties:(nullable NSDictionary<NSString *, id> *)receiptProperties
|
||||
completion:(void (^)(FlutterError *_Nullable))completion;
|
||||
- (void)startObservingPaymentQueueWithError:(FlutterError *_Nullable *_Nonnull)error;
|
||||
- (void)stopObservingPaymentQueueWithError:(FlutterError *_Nullable *_Nonnull)error;
|
||||
- (void)registerPaymentQueueDelegateWithError:(FlutterError *_Nullable *_Nonnull)error;
|
||||
- (void)removePaymentQueueDelegateWithError:(FlutterError *_Nullable *_Nonnull)error;
|
||||
- (void)showPriceConsentIfNeededWithError:(FlutterError *_Nullable *_Nonnull)error;
|
||||
@end
|
||||
|
||||
extern void SetUpInAppPurchaseAPI(id<FlutterBinaryMessenger> binaryMessenger,
|
||||
|
@ -1,7 +1,6 @@
|
||||
// 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
|
||||
|
||||
@ -752,4 +751,147 @@ void SetUpInAppPurchaseAPI(id<FlutterBinaryMessenger> binaryMessenger,
|
||||
[channel setMessageHandler:nil];
|
||||
}
|
||||
}
|
||||
{
|
||||
FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc]
|
||||
initWithName:
|
||||
@"dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.retrieveReceiptData"
|
||||
binaryMessenger:binaryMessenger
|
||||
codec:InAppPurchaseAPIGetCodec()];
|
||||
if (api) {
|
||||
NSCAssert(
|
||||
[api respondsToSelector:@selector(retrieveReceiptDataWithError:)],
|
||||
@"InAppPurchaseAPI api (%@) doesn't respond to @selector(retrieveReceiptDataWithError:)",
|
||||
api);
|
||||
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
|
||||
FlutterError *error;
|
||||
NSString *output = [api retrieveReceiptDataWithError:&error];
|
||||
callback(wrapResult(output, error));
|
||||
}];
|
||||
} else {
|
||||
[channel setMessageHandler:nil];
|
||||
}
|
||||
}
|
||||
{
|
||||
FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc]
|
||||
initWithName:
|
||||
@"dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.refreshReceipt"
|
||||
binaryMessenger:binaryMessenger
|
||||
codec:InAppPurchaseAPIGetCodec()];
|
||||
if (api) {
|
||||
NSCAssert([api respondsToSelector:@selector(refreshReceiptReceiptProperties:completion:)],
|
||||
@"InAppPurchaseAPI api (%@) doesn't respond to "
|
||||
@"@selector(refreshReceiptReceiptProperties:completion:)",
|
||||
api);
|
||||
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
|
||||
NSArray *args = message;
|
||||
NSDictionary<NSString *, id> *arg_receiptProperties = GetNullableObjectAtIndex(args, 0);
|
||||
[api refreshReceiptReceiptProperties:arg_receiptProperties
|
||||
completion:^(FlutterError *_Nullable error) {
|
||||
callback(wrapResult(nil, error));
|
||||
}];
|
||||
}];
|
||||
} else {
|
||||
[channel setMessageHandler:nil];
|
||||
}
|
||||
}
|
||||
{
|
||||
FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc]
|
||||
initWithName:@"dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI."
|
||||
@"startObservingPaymentQueue"
|
||||
binaryMessenger:binaryMessenger
|
||||
codec:InAppPurchaseAPIGetCodec()];
|
||||
if (api) {
|
||||
NSCAssert([api respondsToSelector:@selector(startObservingPaymentQueueWithError:)],
|
||||
@"InAppPurchaseAPI api (%@) doesn't respond to "
|
||||
@"@selector(startObservingPaymentQueueWithError:)",
|
||||
api);
|
||||
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
|
||||
FlutterError *error;
|
||||
[api startObservingPaymentQueueWithError:&error];
|
||||
callback(wrapResult(nil, error));
|
||||
}];
|
||||
} else {
|
||||
[channel setMessageHandler:nil];
|
||||
}
|
||||
}
|
||||
{
|
||||
FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc]
|
||||
initWithName:@"dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI."
|
||||
@"stopObservingPaymentQueue"
|
||||
binaryMessenger:binaryMessenger
|
||||
codec:InAppPurchaseAPIGetCodec()];
|
||||
if (api) {
|
||||
NSCAssert([api respondsToSelector:@selector(stopObservingPaymentQueueWithError:)],
|
||||
@"InAppPurchaseAPI api (%@) doesn't respond to "
|
||||
@"@selector(stopObservingPaymentQueueWithError:)",
|
||||
api);
|
||||
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
|
||||
FlutterError *error;
|
||||
[api stopObservingPaymentQueueWithError:&error];
|
||||
callback(wrapResult(nil, error));
|
||||
}];
|
||||
} else {
|
||||
[channel setMessageHandler:nil];
|
||||
}
|
||||
}
|
||||
{
|
||||
FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc]
|
||||
initWithName:@"dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI."
|
||||
@"registerPaymentQueueDelegate"
|
||||
binaryMessenger:binaryMessenger
|
||||
codec:InAppPurchaseAPIGetCodec()];
|
||||
if (api) {
|
||||
NSCAssert([api respondsToSelector:@selector(registerPaymentQueueDelegateWithError:)],
|
||||
@"InAppPurchaseAPI api (%@) doesn't respond to "
|
||||
@"@selector(registerPaymentQueueDelegateWithError:)",
|
||||
api);
|
||||
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
|
||||
FlutterError *error;
|
||||
[api registerPaymentQueueDelegateWithError:&error];
|
||||
callback(wrapResult(nil, error));
|
||||
}];
|
||||
} else {
|
||||
[channel setMessageHandler:nil];
|
||||
}
|
||||
}
|
||||
{
|
||||
FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc]
|
||||
initWithName:@"dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI."
|
||||
@"removePaymentQueueDelegate"
|
||||
binaryMessenger:binaryMessenger
|
||||
codec:InAppPurchaseAPIGetCodec()];
|
||||
if (api) {
|
||||
NSCAssert([api respondsToSelector:@selector(removePaymentQueueDelegateWithError:)],
|
||||
@"InAppPurchaseAPI api (%@) doesn't respond to "
|
||||
@"@selector(removePaymentQueueDelegateWithError:)",
|
||||
api);
|
||||
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
|
||||
FlutterError *error;
|
||||
[api removePaymentQueueDelegateWithError:&error];
|
||||
callback(wrapResult(nil, error));
|
||||
}];
|
||||
} else {
|
||||
[channel setMessageHandler:nil];
|
||||
}
|
||||
}
|
||||
{
|
||||
FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc]
|
||||
initWithName:@"dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI."
|
||||
@"showPriceConsentIfNeeded"
|
||||
binaryMessenger:binaryMessenger
|
||||
codec:InAppPurchaseAPIGetCodec()];
|
||||
if (api) {
|
||||
NSCAssert([api respondsToSelector:@selector(showPriceConsentIfNeededWithError:)],
|
||||
@"InAppPurchaseAPI api (%@) doesn't respond to "
|
||||
@"@selector(showPriceConsentIfNeededWithError:)",
|
||||
api);
|
||||
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
|
||||
FlutterError *error;
|
||||
[api showPriceConsentIfNeededWithError:&error];
|
||||
callback(wrapResult(nil, error));
|
||||
}];
|
||||
} else {
|
||||
[channel setMessageHandler:nil];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,8 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
codeCoverageEnabled = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
|
@ -26,20 +26,6 @@
|
||||
- (void)tearDown {
|
||||
}
|
||||
|
||||
- (void)testInvalidMethodCall {
|
||||
XCTestExpectation *expectation =
|
||||
[self expectationWithDescription:@"expect result to be not implemented"];
|
||||
FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"invalid" arguments:NULL];
|
||||
__block id result;
|
||||
[self.plugin handleMethodCall:call
|
||||
result:^(id r) {
|
||||
[expectation fulfill];
|
||||
result = r;
|
||||
}];
|
||||
[self waitForExpectations:@[ expectation ] timeout:5];
|
||||
XCTAssertEqual(result, FlutterMethodNotImplemented);
|
||||
}
|
||||
|
||||
- (void)testCanMakePayments {
|
||||
FlutterError *error;
|
||||
NSNumber *result = [self.plugin canMakePaymentsWithError:&error];
|
||||
@ -299,17 +285,8 @@
|
||||
}
|
||||
|
||||
- (void)testRetrieveReceiptDataSuccess {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"receipt data retrieved"];
|
||||
FlutterMethodCall *call = [FlutterMethodCall
|
||||
methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]"
|
||||
arguments:nil];
|
||||
__block NSDictionary *result;
|
||||
[self.plugin handleMethodCall:call
|
||||
result:^(id r) {
|
||||
result = r;
|
||||
[expectation fulfill];
|
||||
}];
|
||||
[self waitForExpectations:@[ expectation ] timeout:5];
|
||||
FlutterError *error;
|
||||
NSString *result = [self.plugin retrieveReceiptDataWithError:&error];
|
||||
XCTAssertNotNil(result);
|
||||
XCTAssert([result isKindOfClass:[NSString class]]);
|
||||
}
|
||||
@ -317,71 +294,47 @@
|
||||
- (void)testRetrieveReceiptDataNil {
|
||||
NSBundle *mockBundle = OCMPartialMock([NSBundle mainBundle]);
|
||||
OCMStub(mockBundle.appStoreReceiptURL).andReturn(nil);
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"nil receipt data retrieved"];
|
||||
FlutterMethodCall *call = [FlutterMethodCall
|
||||
methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]"
|
||||
arguments:nil];
|
||||
__block NSDictionary *result;
|
||||
[self.plugin handleMethodCall:call
|
||||
result:^(id r) {
|
||||
result = r;
|
||||
[expectation fulfill];
|
||||
}];
|
||||
[self waitForExpectations:@[ expectation ] timeout:5];
|
||||
FlutterError *error;
|
||||
NSString *result = [self.plugin retrieveReceiptDataWithError:&error];
|
||||
XCTAssertNil(result);
|
||||
}
|
||||
|
||||
- (void)testRetrieveReceiptDataError {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"receipt data retrieved"];
|
||||
FlutterMethodCall *call = [FlutterMethodCall
|
||||
methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]"
|
||||
arguments:nil];
|
||||
__block NSDictionary *result;
|
||||
self.receiptManagerStub.returnError = YES;
|
||||
[self.plugin handleMethodCall:call
|
||||
result:^(id r) {
|
||||
result = r;
|
||||
[expectation fulfill];
|
||||
}];
|
||||
[self waitForExpectations:@[ expectation ] timeout:5];
|
||||
XCTAssertNotNil(result);
|
||||
XCTAssert([result isKindOfClass:[FlutterError class]]);
|
||||
NSDictionary *details = ((FlutterError *)result).details;
|
||||
|
||||
FlutterError *error;
|
||||
NSString *result = [self.plugin retrieveReceiptDataWithError:&error];
|
||||
|
||||
XCTAssertNil(result);
|
||||
XCTAssertNotNil(error);
|
||||
NSDictionary *details = error.details;
|
||||
XCTAssertNotNil(details[@"error"]);
|
||||
NSNumber *errorCode = (NSNumber *)details[@"error"][@"code"];
|
||||
XCTAssertEqual(errorCode, [NSNumber numberWithInteger:99]);
|
||||
}
|
||||
|
||||
- (void)testRefreshReceiptRequest {
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"expect success"];
|
||||
FlutterMethodCall *call =
|
||||
[FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin refreshReceipt:result:]"
|
||||
arguments:nil];
|
||||
__block BOOL result = NO;
|
||||
[self.plugin handleMethodCall:call
|
||||
result:^(id r) {
|
||||
result = YES;
|
||||
XCTestExpectation *expectation =
|
||||
[self expectationWithDescription:@"completion handler successfully called"];
|
||||
[self.plugin refreshReceiptReceiptProperties:nil
|
||||
completion:^(FlutterError *_Nullable error) {
|
||||
[expectation fulfill];
|
||||
}];
|
||||
[self waitForExpectations:@[ expectation ] timeout:5];
|
||||
XCTAssertTrue(result);
|
||||
}
|
||||
|
||||
/// presentCodeRedemptionSheetWithError:error is only available on iOS
|
||||
#if TARGET_OS_IOS
|
||||
- (void)testPresentCodeRedemptionSheet {
|
||||
XCTestExpectation *expectation =
|
||||
[self expectationWithDescription:@"expect successfully present Code Redemption Sheet"];
|
||||
FlutterMethodCall *call = [FlutterMethodCall
|
||||
methodCallWithMethodName:@"-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]"
|
||||
arguments:nil];
|
||||
__block BOOL callbackInvoked = NO;
|
||||
[self.plugin handleMethodCall:call
|
||||
result:^(id r) {
|
||||
callbackInvoked = YES;
|
||||
[expectation fulfill];
|
||||
}];
|
||||
[self waitForExpectations:@[ expectation ] timeout:5];
|
||||
XCTAssertTrue(callbackInvoked);
|
||||
FIAPaymentQueueHandler *mockHandler = OCMClassMock([FIAPaymentQueueHandler class]);
|
||||
self.plugin.paymentQueueHandler = mockHandler;
|
||||
|
||||
FlutterError *error;
|
||||
[self.plugin presentCodeRedemptionSheetWithError:&error];
|
||||
|
||||
OCMVerify(times(1), [mockHandler presentCodeRedemptionSheet]);
|
||||
}
|
||||
#endif
|
||||
|
||||
- (void)testGetPendingTransactions {
|
||||
SKPaymentQueue *mockQueue = OCMClassMock(SKPaymentQueue.class);
|
||||
@ -420,48 +373,28 @@
|
||||
}
|
||||
|
||||
- (void)testStartObservingPaymentQueue {
|
||||
XCTestExpectation *expectation =
|
||||
[self expectationWithDescription:@"Should return success result"];
|
||||
FlutterMethodCall *startCall = [FlutterMethodCall
|
||||
methodCallWithMethodName:@"-[SKPaymentQueue startObservingTransactionQueue]"
|
||||
arguments:nil];
|
||||
FIAPaymentQueueHandler *mockHandler = OCMClassMock([FIAPaymentQueueHandler class]);
|
||||
self.plugin.paymentQueueHandler = mockHandler;
|
||||
[self.plugin handleMethodCall:startCall
|
||||
result:^(id _Nullable result) {
|
||||
XCTAssertNil(result);
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:5];
|
||||
FlutterError *error;
|
||||
[self.plugin startObservingPaymentQueueWithError:&error];
|
||||
|
||||
OCMVerify(times(1), [mockHandler startObservingPaymentQueue]);
|
||||
}
|
||||
|
||||
- (void)testStopObservingPaymentQueue {
|
||||
XCTestExpectation *expectation =
|
||||
[self expectationWithDescription:@"Should return success result"];
|
||||
FlutterMethodCall *stopCall =
|
||||
[FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue stopObservingTransactionQueue]"
|
||||
arguments:nil];
|
||||
FIAPaymentQueueHandler *mockHandler = OCMClassMock([FIAPaymentQueueHandler class]);
|
||||
self.plugin.paymentQueueHandler = mockHandler;
|
||||
[self.plugin handleMethodCall:stopCall
|
||||
result:^(id _Nullable result) {
|
||||
XCTAssertNil(result);
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:5];
|
||||
FlutterError *error;
|
||||
[self.plugin stopObservingPaymentQueueWithError:&error];
|
||||
|
||||
OCMVerify(times(1), [mockHandler stopObservingPaymentQueue]);
|
||||
}
|
||||
|
||||
#if TARGET_OS_IOS
|
||||
- (void)testRegisterPaymentQueueDelegate {
|
||||
if (@available(iOS 13, *)) {
|
||||
FlutterMethodCall *call =
|
||||
[FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue registerDelegate]"
|
||||
arguments:nil];
|
||||
|
||||
self.plugin.paymentQueueHandler =
|
||||
[[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueueStub new]
|
||||
transactionsUpdated:nil
|
||||
@ -475,9 +408,8 @@
|
||||
// Verify the delegate is nil before we register one.
|
||||
XCTAssertNil(self.plugin.paymentQueueHandler.delegate);
|
||||
|
||||
[self.plugin handleMethodCall:call
|
||||
result:^(id r){
|
||||
}];
|
||||
FlutterError *error;
|
||||
[self.plugin registerPaymentQueueDelegateWithError:&error];
|
||||
|
||||
// Verify the delegate is not nil after we registered one.
|
||||
XCTAssertNotNil(self.plugin.paymentQueueHandler.delegate);
|
||||
@ -487,10 +419,6 @@
|
||||
|
||||
- (void)testRemovePaymentQueueDelegate {
|
||||
if (@available(iOS 13, *)) {
|
||||
FlutterMethodCall *call =
|
||||
[FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue removeDelegate]"
|
||||
arguments:nil];
|
||||
|
||||
self.plugin.paymentQueueHandler =
|
||||
[[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueueStub new]
|
||||
transactionsUpdated:nil
|
||||
@ -505,9 +433,8 @@
|
||||
// Verify the delegate is not nil before removing it.
|
||||
XCTAssertNotNil(self.plugin.paymentQueueHandler.delegate);
|
||||
|
||||
[self.plugin handleMethodCall:call
|
||||
result:^(id r){
|
||||
}];
|
||||
FlutterError *error;
|
||||
[self.plugin removePaymentQueueDelegateWithError:&error];
|
||||
|
||||
// Verify the delegate is nill after removing it.
|
||||
XCTAssertNil(self.plugin.paymentQueueHandler.delegate);
|
||||
@ -516,16 +443,11 @@
|
||||
|
||||
#if TARGET_OS_IOS
|
||||
- (void)testShowPriceConsentIfNeeded {
|
||||
FlutterMethodCall *call =
|
||||
[FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue showPriceConsentIfNeeded]"
|
||||
arguments:nil];
|
||||
|
||||
FIAPaymentQueueHandler *mockQueueHandler = OCMClassMock(FIAPaymentQueueHandler.class);
|
||||
self.plugin.paymentQueueHandler = mockQueueHandler;
|
||||
|
||||
[self.plugin handleMethodCall:call
|
||||
result:^(id r){
|
||||
}];
|
||||
FlutterError *error;
|
||||
[self.plugin showPriceConsentIfNeededWithError:&error];
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||
|
@ -1,3 +1,6 @@
|
||||
// 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
|
||||
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
||||
@ -788,4 +791,173 @@ class InAppPurchaseAPI {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> retrieveReceiptData() async {
|
||||
const String __pigeon_channelName =
|
||||
'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.retrieveReceiptData';
|
||||
final BasicMessageChannel<Object?> __pigeon_channel =
|
||||
BasicMessageChannel<Object?>(
|
||||
__pigeon_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: __pigeon_binaryMessenger,
|
||||
);
|
||||
final List<Object?>? __pigeon_replyList =
|
||||
await __pigeon_channel.send(null) as List<Object?>?;
|
||||
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 (__pigeon_replyList[0] as String?);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refreshReceipt(
|
||||
{Map<String?, Object?>? receiptProperties}) async {
|
||||
const String __pigeon_channelName =
|
||||
'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.refreshReceipt';
|
||||
final BasicMessageChannel<Object?> __pigeon_channel =
|
||||
BasicMessageChannel<Object?>(
|
||||
__pigeon_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: __pigeon_binaryMessenger,
|
||||
);
|
||||
final List<Object?>? __pigeon_replyList = await __pigeon_channel
|
||||
.send(<Object?>[receiptProperties]) as List<Object?>?;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> startObservingPaymentQueue() async {
|
||||
const String __pigeon_channelName =
|
||||
'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.startObservingPaymentQueue';
|
||||
final BasicMessageChannel<Object?> __pigeon_channel =
|
||||
BasicMessageChannel<Object?>(
|
||||
__pigeon_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: __pigeon_binaryMessenger,
|
||||
);
|
||||
final List<Object?>? __pigeon_replyList =
|
||||
await __pigeon_channel.send(null) as List<Object?>?;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stopObservingPaymentQueue() async {
|
||||
const String __pigeon_channelName =
|
||||
'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.stopObservingPaymentQueue';
|
||||
final BasicMessageChannel<Object?> __pigeon_channel =
|
||||
BasicMessageChannel<Object?>(
|
||||
__pigeon_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: __pigeon_binaryMessenger,
|
||||
);
|
||||
final List<Object?>? __pigeon_replyList =
|
||||
await __pigeon_channel.send(null) as List<Object?>?;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> registerPaymentQueueDelegate() async {
|
||||
const String __pigeon_channelName =
|
||||
'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.registerPaymentQueueDelegate';
|
||||
final BasicMessageChannel<Object?> __pigeon_channel =
|
||||
BasicMessageChannel<Object?>(
|
||||
__pigeon_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: __pigeon_binaryMessenger,
|
||||
);
|
||||
final List<Object?>? __pigeon_replyList =
|
||||
await __pigeon_channel.send(null) as List<Object?>?;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> removePaymentQueueDelegate() async {
|
||||
const String __pigeon_channelName =
|
||||
'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.removePaymentQueueDelegate';
|
||||
final BasicMessageChannel<Object?> __pigeon_channel =
|
||||
BasicMessageChannel<Object?>(
|
||||
__pigeon_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: __pigeon_binaryMessenger,
|
||||
);
|
||||
final List<Object?>? __pigeon_replyList =
|
||||
await __pigeon_channel.send(null) as List<Object?>?;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showPriceConsentIfNeeded() async {
|
||||
const String __pigeon_channelName =
|
||||
'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.showPriceConsentIfNeeded';
|
||||
final BasicMessageChannel<Object?> __pigeon_channel =
|
||||
BasicMessageChannel<Object?>(
|
||||
__pigeon_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: __pigeon_binaryMessenger,
|
||||
);
|
||||
final List<Object?>? __pigeon_replyList =
|
||||
await __pigeon_channel.send(null) as List<Object?>?;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -86,16 +86,16 @@ class SKPaymentQueueWrapper {
|
||||
///
|
||||
/// Call this method when the first listener is subscribed to the
|
||||
/// [InAppPurchaseStoreKitPlatform.purchaseStream].
|
||||
Future<void> startObservingTransactionQueue() => channel
|
||||
.invokeMethod<void>('-[SKPaymentQueue startObservingTransactionQueue]');
|
||||
Future<void> startObservingTransactionQueue() =>
|
||||
_hostApi.startObservingPaymentQueue();
|
||||
|
||||
/// Instructs the iOS implementation to remove the transaction observer and
|
||||
/// stop listening to it.
|
||||
///
|
||||
/// Call this when there are no longer any listeners subscribed to the
|
||||
/// [InAppPurchaseStoreKitPlatform.purchaseStream].
|
||||
Future<void> stopObservingTransactionQueue() => channel
|
||||
.invokeMethod<void>('-[SKPaymentQueue stopObservingTransactionQueue]');
|
||||
Future<void> stopObservingTransactionQueue() =>
|
||||
_hostApi.stopObservingPaymentQueue();
|
||||
|
||||
/// Sets an implementation of the [SKPaymentQueueDelegateWrapper].
|
||||
///
|
||||
@ -109,10 +109,10 @@ class SKPaymentQueueWrapper {
|
||||
/// default behaviour will apply (see [documentation](https://developer.apple.com/documentation/storekit/skpaymentqueue/3182429-delegate?language=objc)).
|
||||
Future<void> setDelegate(SKPaymentQueueDelegateWrapper? delegate) async {
|
||||
if (delegate == null) {
|
||||
await channel.invokeMethod<void>('-[SKPaymentQueue removeDelegate]');
|
||||
await _hostApi.removePaymentQueueDelegate();
|
||||
paymentQueueDelegateChannel.setMethodCallHandler(null);
|
||||
} else {
|
||||
await channel.invokeMethod<void>('-[SKPaymentQueue registerDelegate]');
|
||||
await _hostApi.registerPaymentQueueDelegate();
|
||||
paymentQueueDelegateChannel
|
||||
.setMethodCallHandler(handlePaymentQueueDelegateCallbacks);
|
||||
}
|
||||
@ -207,8 +207,7 @@ class SKPaymentQueueWrapper {
|
||||
///
|
||||
/// See documentation of StoreKit's [`-[SKPaymentQueue showPriceConsentIfNeeded]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/3521327-showpriceconsentifneeded?language=objc).
|
||||
Future<void> showPriceConsentIfNeeded() async {
|
||||
await channel
|
||||
.invokeMethod<void>('-[SKPaymentQueue showPriceConsentIfNeeded]');
|
||||
await _hostApi.showPriceConsentIfNeeded();
|
||||
}
|
||||
|
||||
/// Triage a method channel call from the platform and triggers the correct observer method.
|
||||
@ -354,7 +353,7 @@ class SKError {
|
||||
///
|
||||
/// Any key of the map must be a valid [NSErrorUserInfoKey](https://developer.apple.com/documentation/foundation/nserroruserinfokey?language=objc).
|
||||
@JsonKey(defaultValue: <String, dynamic>{})
|
||||
final Map<String?, Object?> userInfo;
|
||||
final Map<String?, Object?>? userInfo;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
|
@ -4,7 +4,9 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import '../channel.dart';
|
||||
import '../messages.g.dart';
|
||||
|
||||
InAppPurchaseAPI _hostApi = InAppPurchaseAPI();
|
||||
|
||||
// ignore: avoid_classes_with_only_static_members
|
||||
/// This class contains static methods to manage StoreKit receipts.
|
||||
@ -17,8 +19,6 @@ class SKReceiptManager {
|
||||
/// For more details on how to validate the receipt data, you can refer to Apple's document about [`About Receipt Validation`](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Introduction.html#//apple_ref/doc/uid/TP40010573-CH105-SW1).
|
||||
/// If the receipt is invalid or missing, you can use [SKRequestMaker.startRefreshReceiptRequest] to request a new receipt.
|
||||
static Future<String> retrieveReceiptData() async {
|
||||
return (await channel.invokeMethod<String>(
|
||||
'-[InAppPurchasePlugin retrieveReceiptData:result:]')) ??
|
||||
'';
|
||||
return (await _hostApi.retrieveReceiptData()) ?? '';
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../channel.dart';
|
||||
import '../messages.g.dart';
|
||||
import 'sk_product_wrapper.dart';
|
||||
|
||||
@ -54,9 +53,6 @@ class SKRequestMaker {
|
||||
/// * isVolumePurchase: whether the receipt is a Volume Purchase Plan receipt.
|
||||
Future<void> startRefreshReceiptRequest(
|
||||
{Map<String, dynamic>? receiptProperties}) {
|
||||
return channel.invokeMethod<void>(
|
||||
'-[InAppPurchasePlugin refreshReceipt:result:]',
|
||||
receiptProperties,
|
||||
);
|
||||
return _hostApi.refreshReceipt(receiptProperties: receiptProperties);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,3 @@
|
||||
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.
|
@ -243,4 +243,19 @@ abstract class InAppPurchaseAPI {
|
||||
void restoreTransactions(String? applicationUserName);
|
||||
|
||||
void presentCodeRedemptionSheet();
|
||||
|
||||
String? retrieveReceiptData();
|
||||
|
||||
@async
|
||||
void refreshReceipt({Map<String, Object?>? receiptProperties});
|
||||
|
||||
void startObservingPaymentQueue();
|
||||
|
||||
void stopObservingPaymentQueue();
|
||||
|
||||
void registerPaymentQueueDelegate();
|
||||
|
||||
void removePaymentQueueDelegate();
|
||||
|
||||
void showPriceConsentIfNeeded();
|
||||
}
|
||||
|
@ -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.11
|
||||
version: 0.3.12
|
||||
|
||||
environment:
|
||||
sdk: ^3.2.3
|
||||
|
@ -5,7 +5,6 @@
|
||||
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';
|
||||
|
||||
@ -13,11 +12,6 @@ import '../store_kit_wrappers/sk_test_stub_objects.dart';
|
||||
import '../test_api.g.dart';
|
||||
|
||||
class FakeStoreKitPlatform implements TestInAppPurchaseApi {
|
||||
FakeStoreKitPlatform() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, onMethodCall);
|
||||
}
|
||||
|
||||
// pre-configured store information
|
||||
String? receiptData;
|
||||
late Set<String> validProductIDs;
|
||||
@ -32,6 +26,7 @@ class FakeStoreKitPlatform implements TestInAppPurchaseApi {
|
||||
SKError? testRestoredError;
|
||||
bool queueIsActive = false;
|
||||
Map<String, dynamic> discountReceived = <String, dynamic>{};
|
||||
bool isPaymentQueueDelegateRegistered = false;
|
||||
|
||||
void reset() {
|
||||
transactionList = <SKPaymentTransactionWrapper>[];
|
||||
@ -57,6 +52,7 @@ class FakeStoreKitPlatform implements TestInAppPurchaseApi {
|
||||
testRestoredError = null;
|
||||
queueIsActive = false;
|
||||
discountReceived = <String, dynamic>{};
|
||||
isPaymentQueueDelegateRegistered = false;
|
||||
}
|
||||
|
||||
SKPaymentTransactionWrapper createPendingTransaction(String id,
|
||||
@ -120,25 +116,6 @@ class FakeStoreKitPlatform implements TestInAppPurchaseApi {
|
||||
transactionIdentifier: transactionId);
|
||||
}
|
||||
|
||||
Future<dynamic> onMethodCall(MethodCall call) {
|
||||
switch (call.method) {
|
||||
case '-[InAppPurchasePlugin retrieveReceiptData:result:]':
|
||||
if (receiptData != null) {
|
||||
return Future<String>.value(receiptData!);
|
||||
} else {
|
||||
throw PlatformException(code: 'no_receipt_data');
|
||||
}
|
||||
case '-[InAppPurchasePlugin refreshReceipt:result:]':
|
||||
receiptData = 'refreshed receipt data';
|
||||
return Future<void>.sync(() {});
|
||||
case '-[SKPaymentQueue startObservingTransactionQueue]':
|
||||
queueIsActive = true;
|
||||
case '-[SKPaymentQueue stopObservingTransactionQueue]':
|
||||
queueIsActive = false;
|
||||
}
|
||||
return Future<void>.sync(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
bool canMakePayments() {
|
||||
return true;
|
||||
@ -246,4 +223,42 @@ class FakeStoreKitPlatform implements TestInAppPurchaseApi {
|
||||
return Future<SKProductsResponseMessage>.value(
|
||||
SkProductResponseWrapper.convertToPigeon(response));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> refreshReceipt({Map<String?, dynamic>? receiptProperties}) {
|
||||
receiptData = 'refreshed receipt data';
|
||||
return Future<void>.sync(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void registerPaymentQueueDelegate() {
|
||||
isPaymentQueueDelegateRegistered = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void removePaymentQueueDelegate() {
|
||||
isPaymentQueueDelegateRegistered = false;
|
||||
}
|
||||
|
||||
@override
|
||||
String retrieveReceiptData() {
|
||||
if (receiptData != null) {
|
||||
return receiptData!;
|
||||
} else {
|
||||
throw PlatformException(code: 'no_receipt_data');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void showPriceConsentIfNeeded() {}
|
||||
|
||||
@override
|
||||
void startObservingPaymentQueue() {
|
||||
queueIsActive = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void stopObservingPaymentQueue() {
|
||||
queueIsActive = false;
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart';
|
||||
import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart';
|
||||
@ -17,9 +16,6 @@ void main() {
|
||||
|
||||
setUpAll(() {
|
||||
TestInAppPurchaseApi.setup(fakeStoreKitPlatform);
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(
|
||||
SystemChannels.platform, fakeStoreKitPlatform.onMethodCall);
|
||||
});
|
||||
|
||||
group('present code redemption sheet', () {
|
||||
|
@ -23,9 +23,6 @@ void main() {
|
||||
|
||||
setUpAll(() {
|
||||
TestInAppPurchaseApi.setup(fakeStoreKitPlatform);
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(
|
||||
SystemChannels.platform, fakeStoreKitPlatform.onMethodCall);
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
|
@ -4,7 +4,6 @@
|
||||
|
||||
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';
|
||||
@ -17,9 +16,6 @@ void main() {
|
||||
|
||||
setUpAll(() {
|
||||
TestInAppPurchaseApi.setup(fakeStoreKitPlatform);
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(
|
||||
SystemChannels.platform, fakeStoreKitPlatform.onMethodCall);
|
||||
});
|
||||
|
||||
setUp(() {});
|
||||
@ -76,10 +72,10 @@ void main() {
|
||||
});
|
||||
|
||||
test('refreshed receipt', () async {
|
||||
final int receiptCountBefore = fakeStoreKitPlatform.refreshReceipt;
|
||||
final int receiptCountBefore = fakeStoreKitPlatform.refreshReceiptCount;
|
||||
await SKRequestMaker().startRefreshReceiptRequest(
|
||||
receiptProperties: <String, dynamic>{'isExpired': true});
|
||||
expect(fakeStoreKitPlatform.refreshReceipt, receiptCountBefore + 1);
|
||||
expect(fakeStoreKitPlatform.refreshReceiptCount, receiptCountBefore + 1);
|
||||
expect(fakeStoreKitPlatform.refreshReceiptParam,
|
||||
<String, dynamic>{'isExpired': true});
|
||||
});
|
||||
@ -175,9 +171,9 @@ void main() {
|
||||
});
|
||||
|
||||
test('showPriceConsentIfNeeded should call methodChannel', () async {
|
||||
expect(fakeStoreKitPlatform.showPriceConsentIfNeeded, false);
|
||||
expect(fakeStoreKitPlatform.showPriceConsent, false);
|
||||
await SKPaymentQueueWrapper().showPriceConsentIfNeeded();
|
||||
expect(fakeStoreKitPlatform.showPriceConsentIfNeeded, true);
|
||||
expect(fakeStoreKitPlatform.showPriceConsent, true);
|
||||
});
|
||||
});
|
||||
|
||||
@ -192,10 +188,6 @@ void main() {
|
||||
}
|
||||
|
||||
class FakeStoreKitPlatform implements TestInAppPurchaseApi {
|
||||
FakeStoreKitPlatform() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, onMethodCall);
|
||||
}
|
||||
// get product request
|
||||
List<dynamic> startProductRequestParam = <dynamic>[];
|
||||
bool getProductRequestFailTest = false;
|
||||
@ -205,7 +197,7 @@ class FakeStoreKitPlatform implements TestInAppPurchaseApi {
|
||||
bool getReceiptFailTest = false;
|
||||
|
||||
// refresh receipt request
|
||||
int refreshReceipt = 0;
|
||||
int refreshReceiptCount = 0;
|
||||
late Map<String, dynamic> refreshReceiptParam;
|
||||
|
||||
// payment queue
|
||||
@ -217,7 +209,7 @@ class FakeStoreKitPlatform implements TestInAppPurchaseApi {
|
||||
bool presentCodeRedemption = false;
|
||||
|
||||
// show price consent sheet
|
||||
bool showPriceConsentIfNeeded = false;
|
||||
bool showPriceConsent = false;
|
||||
|
||||
// indicate if the payment queue delegate is registered
|
||||
bool isPaymentQueueDelegateRegistered = false;
|
||||
@ -225,39 +217,6 @@ class FakeStoreKitPlatform implements TestInAppPurchaseApi {
|
||||
// Listen to purchase updates
|
||||
bool? queueIsActive;
|
||||
|
||||
Future<dynamic> onMethodCall(MethodCall call) {
|
||||
switch (call.method) {
|
||||
// request makers
|
||||
case '-[InAppPurchasePlugin refreshReceipt:result:]':
|
||||
refreshReceipt++;
|
||||
refreshReceiptParam = Map.castFrom<dynamic, dynamic, String, dynamic>(
|
||||
call.arguments as Map<dynamic, dynamic>);
|
||||
return Future<void>.sync(() {});
|
||||
// receipt manager
|
||||
case '-[InAppPurchasePlugin retrieveReceiptData:result:]':
|
||||
if (getReceiptFailTest) {
|
||||
throw Exception('some arbitrary error');
|
||||
}
|
||||
return Future<String>.value('receipt data');
|
||||
case '-[SKPaymentQueue startObservingTransactionQueue]':
|
||||
queueIsActive = true;
|
||||
return Future<void>.sync(() {});
|
||||
case '-[SKPaymentQueue stopObservingTransactionQueue]':
|
||||
queueIsActive = false;
|
||||
return Future<void>.sync(() {});
|
||||
case '-[SKPaymentQueue registerDelegate]':
|
||||
isPaymentQueueDelegateRegistered = true;
|
||||
return Future<void>.sync(() {});
|
||||
case '-[SKPaymentQueue removeDelegate]':
|
||||
isPaymentQueueDelegateRegistered = false;
|
||||
return Future<void>.sync(() {});
|
||||
case '-[SKPaymentQueue showPriceConsentIfNeeded]':
|
||||
showPriceConsentIfNeeded = true;
|
||||
return Future<void>.sync(() {});
|
||||
}
|
||||
return Future<dynamic>.error('method not mocked');
|
||||
}
|
||||
|
||||
@override
|
||||
void addPayment(Map<String?, Object?> paymentMap) {
|
||||
payments
|
||||
@ -304,6 +263,47 @@ class FakeStoreKitPlatform implements TestInAppPurchaseApi {
|
||||
}
|
||||
return Future<SKProductsResponseMessage>.value(dummyProductResponseMessage);
|
||||
}
|
||||
|
||||
@override
|
||||
void registerPaymentQueueDelegate() {
|
||||
isPaymentQueueDelegateRegistered = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void removePaymentQueueDelegate() {
|
||||
isPaymentQueueDelegateRegistered = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void startObservingPaymentQueue() {
|
||||
queueIsActive = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void stopObservingPaymentQueue() {
|
||||
queueIsActive = false;
|
||||
}
|
||||
|
||||
@override
|
||||
String retrieveReceiptData() {
|
||||
if (getReceiptFailTest) {
|
||||
throw Exception('some arbitrary error');
|
||||
}
|
||||
return 'receipt data';
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> refreshReceipt({Map<String?, dynamic>? receiptProperties}) {
|
||||
refreshReceiptCount++;
|
||||
refreshReceiptParam =
|
||||
Map.castFrom<dynamic, dynamic, String, dynamic>(receiptProperties!);
|
||||
return Future<void>.sync(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void showPriceConsentIfNeeded() {
|
||||
showPriceConsent = true;
|
||||
}
|
||||
}
|
||||
|
||||
class TestPaymentQueueDelegate extends SKPaymentQueueDelegateWrapper {}
|
||||
|
@ -4,18 +4,18 @@
|
||||
|
||||
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/store_kit_wrappers.dart';
|
||||
|
||||
import '../fakes/fake_storekit_platform.dart';
|
||||
import '../test_api.g.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
final FakeStoreKitPlatform fakeStoreKitPlatform = FakeStoreKitPlatform();
|
||||
|
||||
setUpAll(() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(
|
||||
SystemChannels.platform, fakeStoreKitPlatform.onMethodCall);
|
||||
TestInAppPurchaseApi.setup(fakeStoreKitPlatform);
|
||||
});
|
||||
|
||||
test(
|
||||
@ -146,25 +146,3 @@ class TestPaymentQueueDelegate extends SKPaymentQueueDelegateWrapper {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class FakeStoreKitPlatform {
|
||||
FakeStoreKitPlatform() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, onMethodCall);
|
||||
}
|
||||
|
||||
// indicate if the payment queue delegate is registered
|
||||
bool isPaymentQueueDelegateRegistered = false;
|
||||
|
||||
Future<dynamic> onMethodCall(MethodCall call) {
|
||||
switch (call.method) {
|
||||
case '-[SKPaymentQueue registerDelegate]':
|
||||
isPaymentQueueDelegateRegistered = true;
|
||||
return Future<void>.sync(() {});
|
||||
case '-[SKPaymentQueue removeDelegate]':
|
||||
isPaymentQueueDelegateRegistered = false;
|
||||
return Future<void>.sync(() {});
|
||||
}
|
||||
return Future<dynamic>.error('method not mocked');
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,6 @@
|
||||
// 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
|
||||
// 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
|
||||
@ -102,6 +105,20 @@ abstract class TestInAppPurchaseApi {
|
||||
|
||||
void presentCodeRedemptionSheet();
|
||||
|
||||
String? retrieveReceiptData();
|
||||
|
||||
Future<void> refreshReceipt({Map<String?, Object?>? receiptProperties});
|
||||
|
||||
void startObservingPaymentQueue();
|
||||
|
||||
void stopObservingPaymentQueue();
|
||||
|
||||
void registerPaymentQueueDelegate();
|
||||
|
||||
void removePaymentQueueDelegate();
|
||||
|
||||
void showPriceConsentIfNeeded();
|
||||
|
||||
static void setup(TestInAppPurchaseApi? api,
|
||||
{BinaryMessenger? binaryMessenger}) {
|
||||
{
|
||||
@ -331,5 +348,185 @@ abstract class TestInAppPurchaseApi {
|
||||
});
|
||||
}
|
||||
}
|
||||
{
|
||||
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<
|
||||
Object?>(
|
||||
'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.retrieveReceiptData',
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger);
|
||||
if (api == null) {
|
||||
_testBinaryMessengerBinding!.defaultBinaryMessenger
|
||||
.setMockDecodedMessageHandler<Object?>(__pigeon_channel, null);
|
||||
} else {
|
||||
_testBinaryMessengerBinding!.defaultBinaryMessenger
|
||||
.setMockDecodedMessageHandler<Object?>(__pigeon_channel,
|
||||
(Object? message) async {
|
||||
try {
|
||||
final String? output = api.retrieveReceiptData();
|
||||
return <Object?>[output];
|
||||
} on PlatformException catch (e) {
|
||||
return wrapResponse(error: e);
|
||||
} catch (e) {
|
||||
return wrapResponse(
|
||||
error: PlatformException(code: 'error', message: e.toString()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
{
|
||||
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<
|
||||
Object?>(
|
||||
'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.refreshReceipt',
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger);
|
||||
if (api == null) {
|
||||
_testBinaryMessengerBinding!.defaultBinaryMessenger
|
||||
.setMockDecodedMessageHandler<Object?>(__pigeon_channel, null);
|
||||
} else {
|
||||
_testBinaryMessengerBinding!.defaultBinaryMessenger
|
||||
.setMockDecodedMessageHandler<Object?>(__pigeon_channel,
|
||||
(Object? message) async {
|
||||
assert(message != null,
|
||||
'Argument for dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.refreshReceipt was null.');
|
||||
final List<Object?> args = (message as List<Object?>?)!;
|
||||
final Map<String?, Object?>? arg_receiptProperties =
|
||||
(args[0] as Map<Object?, Object?>?)?.cast<String?, Object?>();
|
||||
try {
|
||||
await api.refreshReceipt(receiptProperties: arg_receiptProperties);
|
||||
return wrapResponse(empty: true);
|
||||
} on PlatformException catch (e) {
|
||||
return wrapResponse(error: e);
|
||||
} catch (e) {
|
||||
return wrapResponse(
|
||||
error: PlatformException(code: 'error', message: e.toString()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
{
|
||||
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<
|
||||
Object?>(
|
||||
'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.startObservingPaymentQueue',
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger);
|
||||
if (api == null) {
|
||||
_testBinaryMessengerBinding!.defaultBinaryMessenger
|
||||
.setMockDecodedMessageHandler<Object?>(__pigeon_channel, null);
|
||||
} else {
|
||||
_testBinaryMessengerBinding!.defaultBinaryMessenger
|
||||
.setMockDecodedMessageHandler<Object?>(__pigeon_channel,
|
||||
(Object? message) async {
|
||||
try {
|
||||
api.startObservingPaymentQueue();
|
||||
return wrapResponse(empty: true);
|
||||
} on PlatformException catch (e) {
|
||||
return wrapResponse(error: e);
|
||||
} catch (e) {
|
||||
return wrapResponse(
|
||||
error: PlatformException(code: 'error', message: e.toString()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
{
|
||||
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<
|
||||
Object?>(
|
||||
'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.stopObservingPaymentQueue',
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger);
|
||||
if (api == null) {
|
||||
_testBinaryMessengerBinding!.defaultBinaryMessenger
|
||||
.setMockDecodedMessageHandler<Object?>(__pigeon_channel, null);
|
||||
} else {
|
||||
_testBinaryMessengerBinding!.defaultBinaryMessenger
|
||||
.setMockDecodedMessageHandler<Object?>(__pigeon_channel,
|
||||
(Object? message) async {
|
||||
try {
|
||||
api.stopObservingPaymentQueue();
|
||||
return wrapResponse(empty: true);
|
||||
} on PlatformException catch (e) {
|
||||
return wrapResponse(error: e);
|
||||
} catch (e) {
|
||||
return wrapResponse(
|
||||
error: PlatformException(code: 'error', message: e.toString()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
{
|
||||
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<
|
||||
Object?>(
|
||||
'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.registerPaymentQueueDelegate',
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger);
|
||||
if (api == null) {
|
||||
_testBinaryMessengerBinding!.defaultBinaryMessenger
|
||||
.setMockDecodedMessageHandler<Object?>(__pigeon_channel, null);
|
||||
} else {
|
||||
_testBinaryMessengerBinding!.defaultBinaryMessenger
|
||||
.setMockDecodedMessageHandler<Object?>(__pigeon_channel,
|
||||
(Object? message) async {
|
||||
try {
|
||||
api.registerPaymentQueueDelegate();
|
||||
return wrapResponse(empty: true);
|
||||
} on PlatformException catch (e) {
|
||||
return wrapResponse(error: e);
|
||||
} catch (e) {
|
||||
return wrapResponse(
|
||||
error: PlatformException(code: 'error', message: e.toString()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
{
|
||||
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<
|
||||
Object?>(
|
||||
'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.removePaymentQueueDelegate',
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger);
|
||||
if (api == null) {
|
||||
_testBinaryMessengerBinding!.defaultBinaryMessenger
|
||||
.setMockDecodedMessageHandler<Object?>(__pigeon_channel, null);
|
||||
} else {
|
||||
_testBinaryMessengerBinding!.defaultBinaryMessenger
|
||||
.setMockDecodedMessageHandler<Object?>(__pigeon_channel,
|
||||
(Object? message) async {
|
||||
try {
|
||||
api.removePaymentQueueDelegate();
|
||||
return wrapResponse(empty: true);
|
||||
} on PlatformException catch (e) {
|
||||
return wrapResponse(error: e);
|
||||
} catch (e) {
|
||||
return wrapResponse(
|
||||
error: PlatformException(code: 'error', message: e.toString()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
{
|
||||
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<
|
||||
Object?>(
|
||||
'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchaseAPI.showPriceConsentIfNeeded',
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger);
|
||||
if (api == null) {
|
||||
_testBinaryMessengerBinding!.defaultBinaryMessenger
|
||||
.setMockDecodedMessageHandler<Object?>(__pigeon_channel, null);
|
||||
} else {
|
||||
_testBinaryMessengerBinding!.defaultBinaryMessenger
|
||||
.setMockDecodedMessageHandler<Object?>(__pigeon_channel,
|
||||
(Object? message) async {
|
||||
try {
|
||||
api.showPriceConsentIfNeeded();
|
||||
return wrapResponse(empty: true);
|
||||
} on PlatformException catch (e) {
|
||||
return wrapResponse(error: e);
|
||||
} catch (e) {
|
||||
return wrapResponse(
|
||||
error: PlatformException(code: 'error', message: e.toString()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user