Improve in-app purchase for MacOS (#12464)
* Add methods to finish transactions * Add a method to get the product descriptions from the App Store * Improve the documentation of a transaction structure * Add a tutorial for In App Purchase * Fix typo in In-App Purchase tutorial * Fix style of In-App Purchase files * Fix In-App-Purchase product structure conversion in amr64 * Fix code style in In-App Purchase tutorial documentation * Fix typos in In-App Purchase documentation * Fix typo in In-App Purchase spec * Slight style fixes
This commit is contained in:
parent
52b1065b3b
commit
5486a65702
13 changed files with 481 additions and 22 deletions
|
@ -45,6 +45,29 @@ struct Converter<in_app_purchase::Transaction> {
|
|||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct Converter<in_app_purchase::Product> {
|
||||
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
|
||||
const in_app_purchase::Product& val) {
|
||||
mate::Dictionary dict = mate::Dictionary::CreateEmpty(isolate);
|
||||
dict.SetHidden("simple", true);
|
||||
dict.Set("productIdentifier", val.productIdentifier);
|
||||
dict.Set("localizedDescription", val.localizedDescription);
|
||||
dict.Set("localizedTitle", val.localizedTitle);
|
||||
dict.Set("contentVersion", val.localizedTitle);
|
||||
dict.Set("contentLengths", val.contentLengths);
|
||||
|
||||
// Pricing Information
|
||||
dict.Set("price", val.price);
|
||||
dict.Set("formattedPrice", val.formattedPrice);
|
||||
|
||||
// Downloadable Content Information
|
||||
dict.Set("isDownloadable", val.downloadable);
|
||||
|
||||
return dict.GetHandle();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace mate
|
||||
|
||||
namespace atom {
|
||||
|
@ -64,7 +87,12 @@ void InAppPurchase::BuildPrototype(v8::Isolate* isolate,
|
|||
mate::ObjectTemplateBuilder(isolate, prototype->PrototypeTemplate())
|
||||
.SetMethod("canMakePayments", &in_app_purchase::CanMakePayments)
|
||||
.SetMethod("getReceiptURL", &in_app_purchase::GetReceiptURL)
|
||||
.SetMethod("purchaseProduct", &InAppPurchase::PurchaseProduct);
|
||||
.SetMethod("purchaseProduct", &InAppPurchase::PurchaseProduct)
|
||||
.SetMethod("finishAllTransactions",
|
||||
&in_app_purchase::FinishAllTransactions)
|
||||
.SetMethod("finishTransactionByDate",
|
||||
&in_app_purchase::FinishTransactionByDate)
|
||||
.SetMethod("getProducts", &in_app_purchase::GetProducts);
|
||||
}
|
||||
|
||||
InAppPurchase::InAppPurchase(v8::Isolate* isolate) {
|
||||
|
|
|
@ -11,14 +11,15 @@
|
|||
#include "atom/browser/api/event_emitter.h"
|
||||
#include "atom/browser/mac/in_app_purchase.h"
|
||||
#include "atom/browser/mac/in_app_purchase_observer.h"
|
||||
#include "atom/browser/mac/in_app_purchase_product.h"
|
||||
#include "native_mate/handle.h"
|
||||
|
||||
namespace atom {
|
||||
|
||||
namespace api {
|
||||
|
||||
class InAppPurchase: public mate::EventEmitter<InAppPurchase>,
|
||||
public in_app_purchase::TransactionObserver {
|
||||
class InAppPurchase : public mate::EventEmitter<InAppPurchase>,
|
||||
public in_app_purchase::TransactionObserver {
|
||||
public:
|
||||
static mate::Handle<InAppPurchase> Create(v8::Isolate* isolate);
|
||||
|
||||
|
|
|
@ -19,6 +19,10 @@ typedef base::Callback<void(bool isProductValid)> InAppPurchaseCallback;
|
|||
|
||||
bool CanMakePayments(void);
|
||||
|
||||
void FinishAllTransactions(void);
|
||||
|
||||
void FinishTransactionByDate(const std::string& date);
|
||||
|
||||
std::string GetReceiptURL(void);
|
||||
|
||||
void PurchaseProduct(const std::string& productID,
|
||||
|
|
|
@ -136,6 +136,34 @@ bool CanMakePayments() {
|
|||
return [SKPaymentQueue canMakePayments];
|
||||
}
|
||||
|
||||
void FinishAllTransactions() {
|
||||
for (SKPaymentTransaction* transaction in SKPaymentQueue.defaultQueue
|
||||
.transactions) {
|
||||
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
|
||||
}
|
||||
}
|
||||
|
||||
void FinishTransactionByDate(const std::string& date) {
|
||||
// Create the date formatter.
|
||||
NSDateFormatter* dateFormatter = [[NSDateFormatter alloc] init];
|
||||
NSLocale* enUSPOSIXLocale =
|
||||
[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
|
||||
[dateFormatter setLocale:enUSPOSIXLocale];
|
||||
[dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZZZZZ"];
|
||||
|
||||
// Remove the transaction.
|
||||
NSString* transactionDate = base::SysUTF8ToNSString(date);
|
||||
|
||||
for (SKPaymentTransaction* transaction in SKPaymentQueue.defaultQueue
|
||||
.transactions) {
|
||||
if ([transactionDate
|
||||
isEqualToString:[dateFormatter
|
||||
stringFromDate:transaction.transactionDate]]) {
|
||||
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string GetReceiptURL() {
|
||||
NSURL* receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
|
||||
if (receiptURL != nil) {
|
||||
|
|
|
@ -17,9 +17,8 @@
|
|||
|
||||
namespace {
|
||||
|
||||
using InAppTransactionCallback =
|
||||
base::RepeatingCallback<
|
||||
void(const std::vector<in_app_purchase::Transaction>&)>;
|
||||
using InAppTransactionCallback = base::RepeatingCallback<void(
|
||||
const std::vector<in_app_purchase::Transaction>&)>;
|
||||
|
||||
} // namespace
|
||||
|
||||
|
@ -72,8 +71,8 @@ using InAppTransactionCallback =
|
|||
}
|
||||
|
||||
// Send the callback to the browser thread.
|
||||
content::BrowserThread::PostTask(
|
||||
content::BrowserThread::UI, FROM_HERE, base::Bind(callback_, converted));
|
||||
content::BrowserThread::PostTask(content::BrowserThread::UI, FROM_HERE,
|
||||
base::Bind(callback_, converted));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -141,9 +140,9 @@ using InAppTransactionCallback =
|
|||
}
|
||||
|
||||
if (transaction.transactionState < 5) {
|
||||
transactionStruct.transactionState = [[@[
|
||||
@"purchasing", @"purchased", @"failed", @"restored", @"deferred"
|
||||
] objectAtIndex:transaction.transactionState] UTF8String];
|
||||
transactionStruct.transactionState =
|
||||
[[@[ @"purchasing", @"purchased", @"failed", @"restored", @"deferred" ]
|
||||
objectAtIndex:transaction.transactionState] UTF8String];
|
||||
}
|
||||
|
||||
if (transaction.payment != nil) {
|
||||
|
@ -177,8 +176,8 @@ namespace in_app_purchase {
|
|||
|
||||
TransactionObserver::TransactionObserver() : weak_ptr_factory_(this) {
|
||||
obeserver_ = [[InAppTransactionObserver alloc]
|
||||
initWithCallback:base::Bind(&TransactionObserver::OnTransactionsUpdated,
|
||||
weak_ptr_factory_.GetWeakPtr())];
|
||||
initWithCallback:base::Bind(&TransactionObserver::OnTransactionsUpdated,
|
||||
weak_ptr_factory_.GetWeakPtr())];
|
||||
}
|
||||
|
||||
TransactionObserver::~TransactionObserver() {
|
||||
|
|
47
atom/browser/mac/in_app_purchase_product.h
Normal file
47
atom/browser/mac/in_app_purchase_product.h
Normal file
|
@ -0,0 +1,47 @@
|
|||
// Copyright (c) 2018 Amaplex Software, Inc.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef ATOM_BROWSER_MAC_IN_APP_PURCHASE_PRODUCT_H_
|
||||
#define ATOM_BROWSER_MAC_IN_APP_PURCHASE_PRODUCT_H_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "base/callback.h"
|
||||
|
||||
namespace in_app_purchase {
|
||||
|
||||
// --------------------------- Structures ---------------------------
|
||||
|
||||
struct Product {
|
||||
// Product Identifier
|
||||
std::string productIdentifier;
|
||||
|
||||
// Product Attributes
|
||||
std::string localizedDescription;
|
||||
std::string localizedTitle;
|
||||
std::string contentVersion;
|
||||
std::vector<uint32_t> contentLengths;
|
||||
|
||||
// Pricing Information
|
||||
double price = 0.0;
|
||||
std::string formattedPrice;
|
||||
|
||||
// Downloadable Content Information
|
||||
bool downloadable = false;
|
||||
};
|
||||
|
||||
// --------------------------- Typedefs ---------------------------
|
||||
|
||||
typedef base::Callback<void(const std::vector<in_app_purchase::Product>&)>
|
||||
InAppPurchaseProductsCallback;
|
||||
|
||||
// --------------------------- Functions ---------------------------
|
||||
|
||||
void GetProducts(const std::vector<std::string>& productIDs,
|
||||
const InAppPurchaseProductsCallback& callback);
|
||||
|
||||
} // namespace in_app_purchase
|
||||
|
||||
#endif // ATOM_BROWSER_MAC_IN_APP_PURCHASE_PRODUCT_H_
|
179
atom/browser/mac/in_app_purchase_product.mm
Normal file
179
atom/browser/mac/in_app_purchase_product.mm
Normal file
|
@ -0,0 +1,179 @@
|
|||
// Copyright (c) 2017 Amaplex Software, Inc.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "atom/browser/mac/in_app_purchase_product.h"
|
||||
|
||||
#include "base/bind.h"
|
||||
#include "base/strings/sys_string_conversions.h"
|
||||
#include "content/public/browser/browser_thread.h"
|
||||
|
||||
#import <StoreKit/StoreKit.h>
|
||||
|
||||
// ============================================================================
|
||||
// InAppPurchaseProduct
|
||||
// ============================================================================
|
||||
|
||||
// --------------------------------- Interface --------------------------------
|
||||
|
||||
@interface InAppPurchaseProduct : NSObject<SKProductsRequestDelegate> {
|
||||
@private
|
||||
in_app_purchase::InAppPurchaseProductsCallback callback_;
|
||||
}
|
||||
|
||||
- (id)initWithCallback:
|
||||
(const in_app_purchase::InAppPurchaseProductsCallback&)callback;
|
||||
|
||||
@end
|
||||
|
||||
// ------------------------------- Implementation -----------------------------
|
||||
|
||||
@implementation InAppPurchaseProduct
|
||||
|
||||
/**
|
||||
* Init with a callback.
|
||||
*
|
||||
* @param callback - The callback that will be called to return the products.
|
||||
*/
|
||||
- (id)initWithCallback:
|
||||
(const in_app_purchase::InAppPurchaseProductsCallback&)callback {
|
||||
if ((self = [super init])) {
|
||||
callback_ = callback;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return products.
|
||||
*
|
||||
* @param productIDs - The products' id to fetch.
|
||||
*/
|
||||
- (void)getProducts:(NSSet*)productIDs {
|
||||
SKProductsRequest* productsRequest;
|
||||
productsRequest =
|
||||
[[SKProductsRequest alloc] initWithProductIdentifiers:productIDs];
|
||||
|
||||
productsRequest.delegate = self;
|
||||
[productsRequest start];
|
||||
}
|
||||
|
||||
/**
|
||||
* @see SKProductsRequestDelegate
|
||||
*/
|
||||
- (void)productsRequest:(SKProductsRequest*)request
|
||||
didReceiveResponse:(SKProductsResponse*)response {
|
||||
// Release request object.
|
||||
[request release];
|
||||
|
||||
// Get the products.
|
||||
NSArray* products = response.products;
|
||||
|
||||
// Convert the products.
|
||||
std::vector<in_app_purchase::Product> converted;
|
||||
converted.reserve([products count]);
|
||||
|
||||
for (SKProduct* product in products) {
|
||||
converted.push_back([self skProductToStruct:product]);
|
||||
}
|
||||
|
||||
// Send the callback to the browser thread.
|
||||
content::BrowserThread::PostTask(content::BrowserThread::UI, FROM_HERE,
|
||||
base::Bind(callback_, converted));
|
||||
|
||||
[self release];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format local price.
|
||||
*
|
||||
* @param price - The price to format.
|
||||
* @param priceLocal - The local format.
|
||||
*/
|
||||
- (NSString*)formatPrice:(NSDecimalNumber*)price
|
||||
withLocal:(NSLocale*)priceLocal {
|
||||
NSNumberFormatter* numberFormatter = [[NSNumberFormatter alloc] init];
|
||||
|
||||
[numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
|
||||
[numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
|
||||
[numberFormatter setLocale:priceLocal];
|
||||
|
||||
return [numberFormatter stringFromNumber:price];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a skProduct object to Product structure.
|
||||
*
|
||||
* @param product - The SKProduct object to convert.
|
||||
*/
|
||||
- (in_app_purchase::Product)skProductToStruct:(SKProduct*)product {
|
||||
in_app_purchase::Product productStruct;
|
||||
|
||||
// Product Identifier
|
||||
if (product.productIdentifier != nil) {
|
||||
productStruct.productIdentifier = [product.productIdentifier UTF8String];
|
||||
}
|
||||
|
||||
// Product Attributes
|
||||
if (product.localizedDescription != nil) {
|
||||
productStruct.localizedDescription =
|
||||
[product.localizedDescription UTF8String];
|
||||
}
|
||||
if (product.localizedTitle != nil) {
|
||||
productStruct.localizedTitle = [product.localizedTitle UTF8String];
|
||||
}
|
||||
if (product.contentVersion != nil) {
|
||||
productStruct.contentVersion = [product.contentVersion UTF8String];
|
||||
}
|
||||
if (product.contentLengths != nil) {
|
||||
productStruct.contentLengths.reserve([product.contentLengths count]);
|
||||
|
||||
for (NSNumber* contentLength in product.contentLengths) {
|
||||
productStruct.contentLengths.push_back([contentLength longLongValue]);
|
||||
}
|
||||
}
|
||||
|
||||
// Pricing Information
|
||||
if (product.price != nil) {
|
||||
productStruct.price = [product.price doubleValue];
|
||||
|
||||
if (product.priceLocale != nil) {
|
||||
productStruct.formattedPrice =
|
||||
[[self formatPrice:product.price withLocal:product.priceLocale]
|
||||
UTF8String];
|
||||
}
|
||||
}
|
||||
|
||||
// Downloadable Content Information
|
||||
if (product.downloadable == true) {
|
||||
productStruct.downloadable = true;
|
||||
}
|
||||
|
||||
return productStruct;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
// ============================================================================
|
||||
// C++ in_app_purchase
|
||||
// ============================================================================
|
||||
|
||||
namespace in_app_purchase {
|
||||
|
||||
void GetProducts(const std::vector<std::string>& productIDs,
|
||||
const InAppPurchaseProductsCallback& callback) {
|
||||
auto* iapProduct = [[InAppPurchaseProduct alloc] initWithCallback:callback];
|
||||
|
||||
// Convert the products' id to NSSet.
|
||||
NSMutableSet* productsIDSet =
|
||||
[NSMutableSet setWithCapacity:productIDs.size()];
|
||||
|
||||
for (auto& productID : productIDs) {
|
||||
[productsIDSet addObject:base::SysUTF8ToNSString(productID)];
|
||||
}
|
||||
|
||||
// Fetch the products.
|
||||
[iapProduct getProducts:productsIDSet];
|
||||
}
|
||||
|
||||
} // namespace in_app_purchase
|
Loading…
Add table
Add a link
Reference in a new issue