diff --git a/atom/browser/api/atom_api_in_app_purchase.cc b/atom/browser/api/atom_api_in_app_purchase.cc new file mode 100644 index 00000000000..21bad43ee04 --- /dev/null +++ b/atom/browser/api/atom_api_in_app_purchase.cc @@ -0,0 +1,63 @@ +// 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 +#include +#include + +#include "atom/browser/api/atom_api_in_app_purchase.h" +#include "atom/common/native_mate_converters/callback.h" +#include "native_mate/dictionary.h" + +#include "atom/common/node_includes.h" + +namespace mate { + +v8::Local Converter::ToV8( + v8::Isolate* isolate, + const in_app_purchase::Payment& payment) { + mate::Dictionary dict = mate::Dictionary::CreateEmpty(isolate); + dict.SetHidden("simple", true); + dict.Set("productIdentifier", payment.productIdentifier); + dict.Set("quantity", payment.quantity); + return dict.GetHandle(); +} + +v8::Local Converter::ToV8( + v8::Isolate* isolate, + const in_app_purchase::Transaction& transaction) { + mate::Dictionary dict = mate::Dictionary::CreateEmpty(isolate); + dict.SetHidden("simple", true); + dict.Set("transactionIdentifier", transaction.transactionIdentifier); + dict.Set("transactionDate", transaction.transactionDate); + dict.Set("originalTransactionIdentifier", + transaction.originalTransactionIdentifier); + dict.Set("transactionState", transaction.transactionState); + + dict.Set("errorCode", transaction.errorCode); + dict.Set("errorMessage", transaction.errorMessage); + + return dict.GetHandle(); +} +} // namespace mate + +namespace { + +void Initialize(v8::Local exports, + v8::Local unused, + v8::Local context, + void* priv) { + mate::Dictionary dict(context->GetIsolate(), exports); +#if defined(OS_MACOSX) + dict.SetMethod("canMakePayments", &in_app_purchase::CanMakePayments); + dict.SetMethod("getReceiptURL", &in_app_purchase::GetReceiptURL); + dict.SetMethod("purchaseProduct", &in_app_purchase::PurchaseProduct); + dict.SetMethod("addTransactionListener", + &in_app_purchase::AddTransactionObserver); +#endif +} + +} // namespace + +NODE_MODULE_CONTEXT_AWARE_BUILTIN(atom_browser_in_app_purchase, Initialize) diff --git a/atom/browser/api/atom_api_in_app_purchase.h b/atom/browser/api/atom_api_in_app_purchase.h new file mode 100644 index 00000000000..b1feb7ec266 --- /dev/null +++ b/atom/browser/api/atom_api_in_app_purchase.h @@ -0,0 +1,36 @@ +// 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. + +#ifndef ATOM_BROWSER_API_ATOM_API_IN_APP_PURCHASE_H_ +#define ATOM_BROWSER_API_ATOM_API_IN_APP_PURCHASE_H_ + +#include + +#include "atom/browser/in_app_purchase.h" +#include "atom/browser/in_app_purchase_observer.h" +#include "native_mate/dictionary.h" + +namespace mate { + +template <> +struct Converter { + static v8::Local ToV8(v8::Isolate* isolate, + const in_app_purchase::Payment& val); + static bool FromV8(v8::Isolate* isolate, + v8::Local val, + in_app_purchase::Payment* out); +}; + +template <> +struct Converter { + static v8::Local ToV8(v8::Isolate* isolate, + const in_app_purchase::Transaction& val); + static bool FromV8(v8::Isolate* isolate, + v8::Local val, + in_app_purchase::Transaction* out); +}; + +} // namespace mate + +#endif // ATOM_BROWSER_API_ATOM_API_IN_APP_PURCHASE_H_ diff --git a/atom/browser/in_app_purchase.h b/atom/browser/in_app_purchase.h new file mode 100644 index 00000000000..902d70deacf --- /dev/null +++ b/atom/browser/in_app_purchase.h @@ -0,0 +1,30 @@ +// 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. + +#ifndef ATOM_BROWSER_UI_IN_APP_PURCHASE_H_ +#define ATOM_BROWSER_UI_IN_APP_PURCHASE_H_ + +#include + +#include "base/callback.h" + +namespace in_app_purchase { + +// --------------------------- Typedefs --------------------------- + +typedef base::Callback InAppPurchaseCallback; + +// --------------------------- Functions --------------------------- + +bool CanMakePayments(void); + +std::string GetReceiptURL(void); + +void PurchaseProduct(const std::string& productID, + const int quantity, + const InAppPurchaseCallback& callback); + +} // namespace in_app_purchase + +#endif // ATOM_BROWSER_UI_IN_APP_PURCHASE_H_ diff --git a/atom/browser/in_app_purchase_mac.mm b/atom/browser/in_app_purchase_mac.mm new file mode 100644 index 00000000000..9e859e7faac --- /dev/null +++ b/atom/browser/in_app_purchase_mac.mm @@ -0,0 +1,149 @@ +// 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/in_app_purchase.h" +#include "base/bind.h" +#include "base/strings/sys_string_conversions.h" +#include "content/public/browser/browser_thread.h" + +#import +#import + +// ============================================================================ +// InAppPurchase +// ============================================================================ + +// --------------------------------- Interface -------------------------------- + +@interface InAppPurchase : NSObject { + @private + in_app_purchase::InAppPurchaseCallback callback_; + NSInteger quantity_; +} + +- (id)initWithCallback:(const in_app_purchase::InAppPurchaseCallback&)callback + quantity:(NSInteger)quantity; + +- (void)purchaseProduct:(NSString*)productID; + +@end + +// ------------------------------- Implementation ----------------------------- + +@implementation InAppPurchase + +/** + * Init with a callback. + * + * @param callback - The callback that will be called when the payment is added + * to the queue. + */ +- (id)initWithCallback:(const in_app_purchase::InAppPurchaseCallback&)callback + quantity:(NSInteger)quantity { + if ((self = [super init])) { + callback_ = callback; + quantity_ = quantity; + } + + return self; +} + +/** + * Start the in-app purchase process. + * + * @param productID - The id of the product to purchase (the id of + * com.example.app.product1 is product1). + */ +- (void)purchaseProduct:(NSString*)productID { + // Retrieve the product information. (The products request retrieves, + // information about valid products along with a list of the invalid product + // identifiers, and then calls its delegate to process the result). + SKProductsRequest* productsRequest; + productsRequest = [[SKProductsRequest alloc] + initWithProductIdentifiers:[NSSet setWithObject:productID]]; + + productsRequest.delegate = self; + [productsRequest start]; +} + +/** + * Process product informations and start the payment. + * + * @param request - The product request. + * @param response - The informations about the list of products. + */ +- (void)productsRequest:(SKProductsRequest*)request + didReceiveResponse:(SKProductsResponse*)response { + // Get the first product. + NSArray* products = response.products; + SKProduct* product = [products count] == 1 ? [products firstObject] : nil; + + // Return if the product is not found or invalid. + if (product == nil) { + [self runCallback:false]; + return; + } + + // Start the payment process. + [self checkout:product]; +} + +/** + * Submit a payment request to the App Store. + * + * @param product - The product to purchase. + */ +- (void)checkout:(SKProduct*)product { + // Add the payment to the transaction queue. (The observer will be called + // when the transaction is finished). + SKMutablePayment* payment = [SKMutablePayment paymentWithProduct:product]; + payment.quantity = quantity_; + + [[SKPaymentQueue defaultQueue] addPayment:payment]; + + // Notify that the payment has been added to the queue with success. + [self runCallback:true]; +} + +/** + * Submit a payment request to the App Store. + * + * @param product - The product to purchase. + */ +- (void)runCallback:(bool)isProductValid { + content::BrowserThread::PostTask(content::BrowserThread::UI, FROM_HERE, + base::Bind(callback_, isProductValid)); +} + +@end + +// ============================================================================ +// C++ in_app_purchase +// ============================================================================ + +namespace in_app_purchase { + +bool CanMakePayments() { + return [SKPaymentQueue canMakePayments]; +} + +std::string GetReceiptURL() { + NSURL* receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; + if (receiptURL != nil) { + return [[receiptURL absoluteString] UTF8String]; + } else { + return ""; + } +} + +void PurchaseProduct(const std::string& productID, + const int quantity, + const InAppPurchaseCallback& callback) { + auto iap = + [[InAppPurchase alloc] initWithCallback:callback quantity:quantity]; + + [iap purchaseProduct:base::SysUTF8ToNSString(productID)]; +} + +} // namespace in_app_purchase diff --git a/atom/browser/in_app_purchase_observer.h b/atom/browser/in_app_purchase_observer.h new file mode 100644 index 00000000000..450ee286b34 --- /dev/null +++ b/atom/browser/in_app_purchase_observer.h @@ -0,0 +1,41 @@ +// 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. + +#ifndef ATOM_BROWSER_UI_IN_APP_PURCHASE_OBSERVER_H_ +#define ATOM_BROWSER_UI_IN_APP_PURCHASE_OBSERVER_H_ + +#include + +#include "base/callback.h" + +namespace in_app_purchase { + +// --------------------------- Structures --------------------------- + +struct Payment { + std::string productIdentifier = ""; + int quantity = 1; +}; + +struct Transaction { + std::string transactionIdentifier = ""; + std::string transactionDate = ""; + std::string originalTransactionIdentifier = ""; + int errorCode = 0; + std::string errorMessage = ""; + std::string transactionState = ""; +}; + +// --------------------------- Typedefs --------------------------- + +typedef base::RepeatingCallback + InAppTransactionCallback; + +// --------------------------- Functions --------------------------- + +void AddTransactionObserver(const InAppTransactionCallback& callback); + +} // namespace in_app_purchase + +#endif // ATOM_BROWSER_UI_IN_APP_PURCHASE_OBSERVER_H_ diff --git a/atom/browser/in_app_purchase_observer_mac.mm b/atom/browser/in_app_purchase_observer_mac.mm new file mode 100644 index 00000000000..0ec1b9bb613 --- /dev/null +++ b/atom/browser/in_app_purchase_observer_mac.mm @@ -0,0 +1,182 @@ +// 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/in_app_purchase_observer.h" +#include "base/bind.h" +#include "base/strings/sys_string_conversions.h" +#include "content/public/browser/browser_thread.h" + +#import +#import + +// ============================================================================ +// InAppTransactionObserver +// ============================================================================ + +@interface InAppTransactionObserver : NSObject { + @private + in_app_purchase::InAppTransactionCallback callback_; +} + +- (id)initWithCallback: + (const in_app_purchase::InAppTransactionCallback&)callback; + +@end + +@implementation InAppTransactionObserver + +/** + * Init with a callback. + * + * @param callback - The callback that will be called for each transaction + * update. + */ +- (id)initWithCallback: + (const in_app_purchase::InAppTransactionCallback&)callback { + if ((self = [super init])) { + callback_ = callback; + + [[SKPaymentQueue defaultQueue] addTransactionObserver:self]; + } + + return self; +} + +/** + * Run the callback in the browser thread. + * + * @param transaction - The transaction to pass to the callback. + */ +- (void)runCallback:(SKPaymentTransaction*)transaction { + if (transaction == nil) { + return; + } + + // Convert the payment. + in_app_purchase::Payment paymentStruct; + paymentStruct = [self skPaymentToStruct:transaction.payment]; + + // Convert the transaction. + in_app_purchase::Transaction transactionStruct; + transactionStruct = [self skPaymentTransactionToStruct:transaction]; + + // Send the callback to the browser thread. + content::BrowserThread::PostTask( + content::BrowserThread::UI, FROM_HERE, + base::Bind(callback_, paymentStruct, transactionStruct)); +} + +/** + * Convert an NSDate to ISO String. + * + * @param date - The date to convert. + */ +- (NSString*)dateToISOString:(NSDate*)date { + NSDateFormatter* dateFormatter = [[NSDateFormatter alloc] init]; + NSLocale* enUSPOSIXLocale = + [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; + [dateFormatter setLocale:enUSPOSIXLocale]; + [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZZZZZ"]; + + return [dateFormatter stringFromDate:date]; +} + +/** + * Convert a SKPayment object to a Payment structure. + * + * @param payment - The SKPayment object to convert. + */ +- (in_app_purchase::Payment)skPaymentToStruct:(SKPayment*)payment { + in_app_purchase::Payment paymentStruct; + + if (payment == nil) { + return paymentStruct; + } + + if (payment.productIdentifier != nil) { + paymentStruct.productIdentifier = [payment.productIdentifier UTF8String]; + } + + if (payment.quantity >= 1) { + paymentStruct.quantity = (int)payment.quantity; + } + + return paymentStruct; +} + +/** + * Convert a SKPaymentTransaction object to a Transaction structure. + * + * @param transaction - The SKPaymentTransaction object to convert. + */ +- (in_app_purchase::Transaction)skPaymentTransactionToStruct: + (SKPaymentTransaction*)transaction { + in_app_purchase::Transaction transactionStruct; + + if (transaction == nil) { + return transactionStruct; + } + + if (transaction.transactionIdentifier != nil) { + transactionStruct.transactionIdentifier = + [transaction.transactionIdentifier UTF8String]; + } + + if (transaction.transactionDate != nil) { + transactionStruct.transactionDate = + [[self dateToISOString:transaction.transactionDate] UTF8String]; + } + + if (transaction.originalTransaction != nil) { + transactionStruct.originalTransactionIdentifier = + [transaction.originalTransaction.transactionIdentifier UTF8String]; + } + + if (transaction.error != nil) { + transactionStruct.errorCode = (int)transaction.error.code; + transactionStruct.errorMessage = + [[transaction.error localizedDescription] UTF8String]; + } + + if (transaction.transactionState < 5) { + transactionStruct.transactionState = [[@[ + @"SKPaymentTransactionStatePurchasing", + @"SKPaymentTransactionStatePurchased", @"SKPaymentTransactionStateFailed", + @"SKPaymentTransactionStateRestored", + @"SKPaymentTransactionStateDeferred" + ] objectAtIndex:transaction.transactionState] UTF8String]; + } + + return transactionStruct; +} + +#pragma mark - +#pragma mark SKPaymentTransactionObserver methods + +/** + * Executed when a transaction is updated. + * + * @param queue - The payment queue. + * @param transactions - The list of transactions updated. + */ +- (void)paymentQueue:(SKPaymentQueue*)queue + updatedTransactions:(NSArray*)transactions { + for (SKPaymentTransaction* transaction in transactions) { + [self runCallback:transaction]; + } +} + +@end + +// ============================================================================ +// C++ in_app_purchase +// ============================================================================ + +namespace in_app_purchase { + +void AddTransactionObserver(const InAppTransactionCallback& callback) { + [[InAppTransactionObserver alloc] initWithCallback:callback]; +} + +} // namespace in_app_purchase diff --git a/atom/common/node_bindings.cc b/atom/common/node_bindings.cc index cc4bc554ef9..f184bcf8da3 100644 --- a/atom/common/node_bindings.cc +++ b/atom/common/node_bindings.cc @@ -40,6 +40,7 @@ REFERENCE_MODULE(atom_browser_desktop_capturer); REFERENCE_MODULE(atom_browser_dialog); REFERENCE_MODULE(atom_browser_download_item); REFERENCE_MODULE(atom_browser_global_shortcut); +REFERENCE_MODULE(atom_browser_in_app_purchase); REFERENCE_MODULE(atom_browser_menu); REFERENCE_MODULE(atom_browser_net); REFERENCE_MODULE(atom_browser_power_monitor); diff --git a/docs/api/in-app-purchase.md b/docs/api/in-app-purchase.md new file mode 100644 index 00000000000..effbf22af0f --- /dev/null +++ b/docs/api/in-app-purchase.md @@ -0,0 +1,36 @@ +# inAppPurchase _macOS_ + +Your application should add a listener before to purchase a product. If there are no listener attached to the queue, the payment queue does not synchronize its list of pending transactions with the Apple App Store. + +## Methods + +The `inAppPurchase` module has the following methods: + +### `inAppPurchase.addTransactionListener(listener)` + +* `listener` Function - Called when transactions are updated by the payment queue. + * `payment` Object + * `productIdentifier` String + * `quantity` Integer + * `transaction` Object + * `transactionIdentifier` String + * `transactionDate` String + * `originalTransactionIdentifier` String + * `transactionState` String - The transaction sate (`"SKPaymentTransactionStatePurchasing"`, `"SKPaymentTransactionStatePurchased"`, `"SKPaymentTransactionStateFailed"`, `"SKPaymentTransactionStateRestored"`, or `"SKPaymentTransactionStateDeferred"`) + * `errorCode` Integer + * `errorMessage` String + + +### `inAppPurchase.purchaseProduct(productID, quantity, callback)` +* `productID` String - The id of the product to purchase. (the id of `com.example.app.product1` is `product1`). +* `quantity` Integer (optional) - The number of items the user wants to purchase. +* `callback` Function (optional) - The callback called when the payment is added to the PaymentQueue. (You should add a listener with `inAppPurchase.addTransactionsListener` to get the transaction status). + * `isProductValid` Boolean - Determine if the product is valid and added to the payment queue. + +### `inAppPurchase.canMakePayments()` + +Returns `true` if the user can make a payment and `false` otherwise. + +### `inAppPurchase.getReceiptURL()` + +Returns `String`, the path to the receipt. \ No newline at end of file diff --git a/electron.gyp b/electron.gyp index 4f3554d88a9..fcd003aba5d 100644 --- a/electron.gyp +++ b/electron.gyp @@ -591,6 +591,7 @@ '$(SDKROOT)/System/Library/Frameworks/Security.framework', '$(SDKROOT)/System/Library/Frameworks/SecurityInterface.framework', '$(SDKROOT)/System/Library/Frameworks/ServiceManagement.framework', + '$(SDKROOT)/System/Library/Frameworks/StoreKit.framework', ], }, 'mac_bundle': 1, diff --git a/filenames.gypi b/filenames.gypi index e9f0f43b97e..80eaffaa85a 100644 --- a/filenames.gypi +++ b/filenames.gypi @@ -20,6 +20,7 @@ 'lib/browser/api/exports/electron.js', 'lib/browser/api/global-shortcut.js', 'lib/browser/api/ipc-main.js', + 'lib/browser/api/in-app-purchase.js', 'lib/browser/api/menu-item-roles.js', 'lib/browser/api/menu-item.js', 'lib/browser/api/menu.js', @@ -120,6 +121,8 @@ 'atom/browser/api/atom_api_download_item.h', 'atom/browser/api/atom_api_global_shortcut.cc', 'atom/browser/api/atom_api_global_shortcut.h', + 'atom/browser/api/atom_api_in_app_purchase.cc', + 'atom/browser/api/atom_api_in_app_purchase.h', 'atom/browser/api/atom_api_menu.cc', 'atom/browser/api/atom_api_menu.h', 'atom/browser/api/atom_api_menu_mac.h', @@ -184,6 +187,10 @@ 'atom/browser/atom_browser_context.h', 'atom/browser/atom_download_manager_delegate.cc', 'atom/browser/atom_download_manager_delegate.h', + 'atom/browser/in_app_purchase.h', + 'atom/browser/in_app_purchase_mac.mm', + 'atom/browser/in_app_purchase_observer.h', + 'atom/browser/in_app_purchase_observer_mac.mm', 'atom/browser/atom_browser_main_parts.cc', 'atom/browser/atom_browser_main_parts.h', 'atom/browser/atom_browser_main_parts_mac.mm', diff --git a/lib/browser/api/in-app-purchase.js b/lib/browser/api/in-app-purchase.js new file mode 100644 index 00000000000..a0585e80dd9 --- /dev/null +++ b/lib/browser/api/in-app-purchase.js @@ -0,0 +1,46 @@ +'use strict' + +const binding = process.atomBinding('in_app_purchase') +const v8Util = process.atomBinding('v8_util') + +module.exports = { + + canMakePayments: function() { + return binding.canMakePayments(); + }, + + getReceiptURL: function() { + return binding.getReceiptURL(); + }, + + purchaseProduct: function(productID, quantity, callback) { + + if (typeof productID !== 'string') { + throw new TypeError('productID must be a string') + } + + if (typeof quantity !== 'number') { + quantity = 1 + } + + if (typeof callback !== 'function') { + callback = function() {}; + } + + return binding.purchaseProduct(productID, quantity, callback) + }, + + addTransactionListener: function(callback) { + + if (typeof callback !== 'function') { + throw new TypeError('callback must be a function') + } + return binding.addTransactionListener(callback) + } +} + + // Mark standard asynchronous functions. + v8Util.setHiddenValue( + module.exports.purchaseProduct, 'asynchronous', true) +v8Util.setHiddenValue( + module.exports.addTransactionListener, 'asynchronous', true) diff --git a/lib/browser/api/module-list.js b/lib/browser/api/module-list.js index d8b20c5bec1..e6f398b47d0 100644 --- a/lib/browser/api/module-list.js +++ b/lib/browser/api/module-list.js @@ -8,6 +8,7 @@ module.exports = [ {name: 'dialog', file: 'dialog'}, {name: 'globalShortcut', file: 'global-shortcut'}, {name: 'ipcMain', file: 'ipc-main'}, + {name: 'inAppPurchase', file: 'in-app-purchase'}, {name: 'Menu', file: 'menu'}, {name: 'MenuItem', file: 'menu-item'}, {name: 'net', file: 'net'},