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

Add support for MacOS to in_app_purchase plugin. #2708

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ abstract class InAppPurchaseConnection {
if (Platform.isAndroid) {
_purchaseUpdatedStream =
GooglePlayConnection.instance.purchaseUpdatedStream;
} else if (Platform.isIOS) {
} else if (Platform.isIOS | Platform.isMacOS) {
_purchaseUpdatedStream =
AppStoreConnection.instance.purchaseUpdatedStream;
} else {
Expand Down Expand Up @@ -258,7 +258,7 @@ abstract class InAppPurchaseConnection {

if (Platform.isAndroid) {
_instance = GooglePlayConnection.instance;
} else if (Platform.isIOS) {
} else if (Platform.isIOS | Platform.isMacOS) {
_instance = AppStoreConnection.instance;
} else {
throw UnsupportedError(
Expand Down
35 changes: 35 additions & 0 deletions packages/in_app_purchase/macos/Classes/FIAObjectTranslator.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface FIAObjectTranslator : NSObject

+ (NSDictionary *)getMapFromSKProduct:(SKProduct *)product;

+ (NSDictionary *)getMapFromSKProductSubscriptionPeriod:(SKProductSubscriptionPeriod *)period
API_AVAILABLE(ios(11.2));

+ (NSDictionary *)getMapFromSKProductDiscount:(SKProductDiscount *)discount
API_AVAILABLE(ios(11.2));

+ (NSDictionary *)getMapFromSKProductsResponse:(SKProductsResponse *)productResponse;

+ (NSDictionary *)getMapFromSKPayment:(SKPayment *)payment;

+ (NSDictionary *)getMapFromNSLocale:(NSLocale *)locale;

+ (SKMutablePayment *)getSKMutablePaymentFromMap:(NSDictionary *)map;

+ (NSDictionary *)getMapFromSKPaymentTransaction:(SKPaymentTransaction *)transaction;

+ (NSDictionary *)getMapFromNSError:(NSError *)error;

@end
;

NS_ASSUME_NONNULL_END
172 changes: 172 additions & 0 deletions packages/in_app_purchase/macos/Classes/FIAObjectTranslator.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "FIAObjectTranslator.h"

#pragma mark - SKProduct Coders

@implementation FIAObjectTranslator

+ (NSDictionary *)getMapFromSKProduct:(SKProduct *)product {
if (!product) {
return nil;
}
NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{
@"localizedDescription" : product.localizedDescription ?: [NSNull null],
@"localizedTitle" : product.localizedTitle ?: [NSNull null],
@"productIdentifier" : product.productIdentifier ?: [NSNull null],
@"price" : product.price.description ?: [NSNull null]

}];
// TODO(cyanglaz): NSLocale is a complex object, want to see the actual need of getting this
// expanded to a map. Matching android to only get the currencySymbol for now.
// https://github.com/flutter/flutter/issues/26610
[map setObject:[FIAObjectTranslator getMapFromNSLocale:product.priceLocale] ?: [NSNull null]
forKey:@"priceLocale"];
if (@available(iOS 11.2, *)) {
[map setObject:[FIAObjectTranslator
getMapFromSKProductSubscriptionPeriod:product.subscriptionPeriod]
?: [NSNull null]
forKey:@"subscriptionPeriod"];
}
if (@available(iOS 11.2, *)) {
[map setObject:[FIAObjectTranslator getMapFromSKProductDiscount:product.introductoryPrice]
?: [NSNull null]
forKey:@"introductoryPrice"];
}
if (@available(iOS 12.0, *)) {
[map setObject:product.subscriptionGroupIdentifier ?: [NSNull null]
forKey:@"subscriptionGroupIdentifier"];
}
return map;
}

+ (NSDictionary *)getMapFromSKProductSubscriptionPeriod:(SKProductSubscriptionPeriod *)period {
if (!period) {
return nil;
}
return @{@"numberOfUnits" : @(period.numberOfUnits), @"unit" : @(period.unit)};
}

+ (NSDictionary *)getMapFromSKProductDiscount:(SKProductDiscount *)discount {
if (!discount) {
return nil;
}
NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{
@"price" : discount.price.description ?: [NSNull null],
@"numberOfPeriods" : @(discount.numberOfPeriods),
@"subscriptionPeriod" :
[FIAObjectTranslator getMapFromSKProductSubscriptionPeriod:discount.subscriptionPeriod]
?: [NSNull null],
@"paymentMode" : @(discount.paymentMode)
}];

// TODO(cyanglaz): NSLocale is a complex object, want to see the actual need of getting this
// expanded to a map. Matching android to only get the currencySymbol for now.
// https://github.com/flutter/flutter/issues/26610
[map setObject:[FIAObjectTranslator getMapFromNSLocale:discount.priceLocale] ?: [NSNull null]
forKey:@"priceLocale"];
return map;
}

+ (NSDictionary *)getMapFromSKProductsResponse:(SKProductsResponse *)productResponse {
if (!productResponse) {
return nil;
}
NSMutableArray *productsMapArray = [NSMutableArray new];
for (SKProduct *product in productResponse.products) {
[productsMapArray addObject:[FIAObjectTranslator getMapFromSKProduct:product]];
}
return @{
@"products" : productsMapArray,
@"invalidProductIdentifiers" : productResponse.invalidProductIdentifiers ?: @[]
};
}

+ (NSDictionary *)getMapFromSKPayment:(SKPayment *)payment {
if (!payment) {
return nil;
}
NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{
@"productIdentifier" : payment.productIdentifier ?: [NSNull null],
@"requestData" : payment.requestData ? [[NSString alloc] initWithData:payment.requestData
encoding:NSUTF8StringEncoding]
: [NSNull null],
@"quantity" : @(payment.quantity),
@"applicationUsername" : payment.applicationUsername ?: [NSNull null]
}];
if (@available(iOS 8.3, *)) {
[map setObject:@(payment.simulatesAskToBuyInSandbox) forKey:@"simulatesAskToBuyInSandbox"];
}
return map;
}

+ (NSDictionary *)getMapFromNSLocale:(NSLocale *)locale {
if (!locale) {
return nil;
}
NSMutableDictionary *map = [[NSMutableDictionary alloc] init];
[map setObject:[locale objectForKey:NSLocaleCurrencySymbol] ?: [NSNull null]
forKey:@"currencySymbol"];
[map setObject:[locale objectForKey:NSLocaleCurrencyCode] ?: [NSNull null]
forKey:@"currencyCode"];
return map;
}

+ (SKMutablePayment *)getSKMutablePaymentFromMap:(NSDictionary *)map {
if (!map) {
return nil;
}
SKMutablePayment *payment = [[SKMutablePayment alloc] init];
payment.productIdentifier = map[@"productIdentifier"];
NSString *utf8String = map[@"requestData"];
payment.requestData = [utf8String dataUsingEncoding:NSUTF8StringEncoding];
payment.quantity = [map[@"quantity"] integerValue];
payment.applicationUsername = map[@"applicationUsername"];
if (@available(iOS 8.3, *)) {
payment.simulatesAskToBuyInSandbox = [map[@"simulatesAskToBuyInSandbox"] boolValue];
}
return payment;
}

+ (NSDictionary *)getMapFromSKPaymentTransaction:(SKPaymentTransaction *)transaction {
if (!transaction) {
return nil;
}
NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{
@"error" : [FIAObjectTranslator getMapFromNSError:transaction.error] ?: [NSNull null],
@"payment" : transaction.payment ? [FIAObjectTranslator getMapFromSKPayment:transaction.payment]
: [NSNull null],
@"originalTransaction" : transaction.originalTransaction
? [FIAObjectTranslator getMapFromSKPaymentTransaction:transaction.originalTransaction]
: [NSNull null],
@"transactionTimeStamp" : transaction.transactionDate
? @(transaction.transactionDate.timeIntervalSince1970)
: [NSNull null],
@"transactionIdentifier" : transaction.transactionIdentifier ?: [NSNull null],
@"transactionState" : @(transaction.transactionState)
}];

return map;
}

+ (NSDictionary *)getMapFromNSError:(NSError *)error {
if (!error) {
return nil;
}
NSMutableDictionary *userInfo = [NSMutableDictionary new];
for (NSErrorUserInfoKey key in error.userInfo) {
id value = error.userInfo[key];
if ([value isKindOfClass:[NSError class]]) {
userInfo[key] = [FIAObjectTranslator getMapFromNSError:value];
} else if ([value isKindOfClass:[NSURL class]]) {
userInfo[key] = [value absoluteString];
} else {
userInfo[key] = value;
}
}
return @{@"code" : @(error.code), @"domain" : error.domain ?: @"", @"userInfo" : userInfo};
}

@end
17 changes: 17 additions & 0 deletions packages/in_app_purchase/macos/Classes/FIAPReceiptManager.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@class FlutterError;

@interface FIAPReceiptManager : NSObject

- (nullable NSString *)retrieveReceiptWithError:(FlutterError *_Nullable *_Nullable)error;

@end

NS_ASSUME_NONNULL_END
33 changes: 33 additions & 0 deletions packages/in_app_purchase/macos/Classes/FIAPReceiptManager.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

//
// FIAPReceiptManager.m
// in_app_purchase
//
// Created by Chris Yang on 3/2/19.
//

#import "FIAPReceiptManager.h"
#import <FlutterMacOS/FlutterMacOS.h>

@implementation FIAPReceiptManager

- (NSString *)retrieveReceiptWithError:(FlutterError **)error {
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [self getReceiptData:receiptURL];
if (!receipt) {
*error = [FlutterError errorWithCode:@"storekit_no_receipt"
message:@"Cannot find receipt for the current main bundle."
details:nil];
return nil;
}
return [receipt base64EncodedStringWithOptions:kNilOptions];
}

- (NSData *)getReceiptData:(NSURL *)url {
return [NSData dataWithContentsOfURL:url];
}

@end
20 changes: 20 additions & 0 deletions packages/in_app_purchase/macos/Classes/FIAPRequestHandler.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>

NS_ASSUME_NONNULL_BEGIN

typedef void (^ProductRequestCompletion)(SKProductsResponse *_Nullable response,
NSError *_Nullable errror);

@interface FIAPRequestHandler : NSObject

- (instancetype)initWithRequest:(SKRequest *)request;
- (void)startProductRequestWithCompletionHandler:(ProductRequestCompletion)completion;

@end

NS_ASSUME_NONNULL_END
55 changes: 55 additions & 0 deletions packages/in_app_purchase/macos/Classes/FIAPRequestHandler.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "FIAPRequestHandler.h"
#import <StoreKit/StoreKit.h>

#pragma mark - Main Handler

@interface FIAPRequestHandler () <SKProductsRequestDelegate>

@property(copy, nonatomic) ProductRequestCompletion completion;
@property(strong, nonatomic) SKRequest *request;

@end

@implementation FIAPRequestHandler

- (instancetype)initWithRequest:(SKRequest *)request {
self = [super init];
if (self) {
self.request = request;
request.delegate = self;
}
return self;
}

- (void)startProductRequestWithCompletionHandler:(ProductRequestCompletion)completion {
self.completion = completion;
[self.request start];
}

- (void)productsRequest:(SKProductsRequest *)request
didReceiveResponse:(SKProductsResponse *)response {
if (self.completion) {
self.completion(response, nil);
// set the completion to nil here so self.completion won't be triggered again in
// requestDidFinish for SKProductRequest.
self.completion = nil;
}
}

- (void)requestDidFinish:(SKRequest *)request {
if (self.completion) {
self.completion(nil, nil);
}
}

- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
if (self.completion) {
self.completion(nil, error);
}
}

@end
Loading