From 3e5a98b5f43fe70961eb6452dc8837453c6de1a0 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Wed, 13 Mar 2019 13:56:01 -0700 Subject: [PATCH] feat: promisify In-App Purchase (#17355) * feat: promisify In-App Purchase * use mate::Arguments in GetProducts --- atom/browser/api/atom_api_in_app_purchase.cc | 36 ++++++++++++++++---- atom/browser/api/atom_api_in_app_purchase.h | 7 +++- atom/browser/mac/in_app_purchase.h | 4 +-- atom/browser/mac/in_app_purchase.mm | 17 ++++----- atom/browser/mac/in_app_purchase_product.h | 4 +-- atom/browser/mac/in_app_purchase_product.mm | 14 ++++---- docs/api/in-app-purchase.md | 28 ++++++++++++--- docs/api/promisification.md | 4 +-- docs/tutorial/in-app-purchases.md | 12 +++---- lib/browser/api/in-app-purchase.js | 5 +++ spec/api-in-app-purchase-spec.js | 28 ++++++++++++--- 11 files changed, 113 insertions(+), 46 deletions(-) diff --git a/atom/browser/api/atom_api_in_app_purchase.cc b/atom/browser/api/atom_api_in_app_purchase.cc index fe5e7ee8cda9..30fe57e5b3e2 100644 --- a/atom/browser/api/atom_api_in_app_purchase.cc +++ b/atom/browser/api/atom_api_in_app_purchase.cc @@ -91,7 +91,7 @@ void InAppPurchase::BuildPrototype(v8::Isolate* isolate, &in_app_purchase::FinishAllTransactions) .SetMethod("finishTransactionByDate", &in_app_purchase::FinishTransactionByDate) - .SetMethod("getProducts", &in_app_purchase::GetProducts); + .SetMethod("getProducts", &InAppPurchase::GetProducts); } InAppPurchase::InAppPurchase(v8::Isolate* isolate) { @@ -100,13 +100,37 @@ InAppPurchase::InAppPurchase(v8::Isolate* isolate) { InAppPurchase::~InAppPurchase() {} -void InAppPurchase::PurchaseProduct(const std::string& product_id, - mate::Arguments* args) { +v8::Local InAppPurchase::PurchaseProduct( + const std::string& product_id, + mate::Arguments* args) { + v8::Isolate* isolate = args->isolate(); + atom::util::Promise promise(isolate); + v8::Local handle = promise.GetHandle(); + int quantity = 1; - in_app_purchase::InAppPurchaseCallback callback; args->GetNext(&quantity); - args->GetNext(&callback); - in_app_purchase::PurchaseProduct(product_id, quantity, callback); + + in_app_purchase::PurchaseProduct( + product_id, quantity, + base::BindOnce(atom::util::Promise::ResolvePromise, + std::move(promise))); + + return handle; +} + +v8::Local InAppPurchase::GetProducts( + const std::vector& productIDs, + mate::Arguments* args) { + v8::Isolate* isolate = args->isolate(); + atom::util::Promise promise(isolate); + v8::Local handle = promise.GetHandle(); + + in_app_purchase::GetProducts( + productIDs, base::BindOnce(atom::util::Promise::ResolvePromise< + std::vector>, + std::move(promise))); + + return handle; } void InAppPurchase::OnTransactionsUpdated( diff --git a/atom/browser/api/atom_api_in_app_purchase.h b/atom/browser/api/atom_api_in_app_purchase.h index 10f8f473e42a..24fb710d0a40 100644 --- a/atom/browser/api/atom_api_in_app_purchase.h +++ b/atom/browser/api/atom_api_in_app_purchase.h @@ -12,6 +12,7 @@ #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 "atom/common/promise_util.h" #include "native_mate/handle.h" namespace atom { @@ -30,7 +31,11 @@ class InAppPurchase : public mate::EventEmitter, explicit InAppPurchase(v8::Isolate* isolate); ~InAppPurchase() override; - void PurchaseProduct(const std::string& product_id, mate::Arguments* args); + v8::Local PurchaseProduct(const std::string& product_id, + mate::Arguments* args); + + v8::Local GetProducts(const std::vector& productIDs, + mate::Arguments* args); // TransactionObserver: void OnTransactionsUpdated( diff --git a/atom/browser/mac/in_app_purchase.h b/atom/browser/mac/in_app_purchase.h index f42f172d3acf..852a60199a07 100644 --- a/atom/browser/mac/in_app_purchase.h +++ b/atom/browser/mac/in_app_purchase.h @@ -13,7 +13,7 @@ namespace in_app_purchase { // --------------------------- Typedefs --------------------------- -typedef base::Callback InAppPurchaseCallback; +typedef base::OnceCallback InAppPurchaseCallback; // --------------------------- Functions --------------------------- @@ -27,7 +27,7 @@ std::string GetReceiptURL(void); void PurchaseProduct(const std::string& productID, int quantity, - const InAppPurchaseCallback& callback); + InAppPurchaseCallback callback); } // namespace in_app_purchase diff --git a/atom/browser/mac/in_app_purchase.mm b/atom/browser/mac/in_app_purchase.mm index 1c10ceff10bd..8fafcb2d04c5 100644 --- a/atom/browser/mac/in_app_purchase.mm +++ b/atom/browser/mac/in_app_purchase.mm @@ -25,7 +25,7 @@ NSInteger quantity_; } -- (id)initWithCallback:(const in_app_purchase::InAppPurchaseCallback&)callback +- (id)initWithCallback:(in_app_purchase::InAppPurchaseCallback)callback quantity:(NSInteger)quantity; - (void)purchaseProduct:(NSString*)productID; @@ -42,10 +42,10 @@ * @param callback - The callback that will be called when the payment is added * to the queue. */ -- (id)initWithCallback:(const in_app_purchase::InAppPurchaseCallback&)callback +- (id)initWithCallback:(in_app_purchase::InAppPurchaseCallback)callback quantity:(NSInteger)quantity { if ((self = [super init])) { - callback_ = callback; + callback_ = std::move(callback); quantity_ = quantity; } @@ -119,8 +119,9 @@ */ - (void)runCallback:(bool)isProductValid { if (callback_) { - base::PostTaskWithTraits(FROM_HERE, {content::BrowserThread::UI}, - base::Bind(callback_, isProductValid)); + base::PostTaskWithTraits( + FROM_HERE, {content::BrowserThread::UI}, + base::BindOnce(std::move(callback_), isProductValid)); } // Release this delegate. [self release]; @@ -177,9 +178,9 @@ std::string GetReceiptURL() { void PurchaseProduct(const std::string& productID, int quantity, - const InAppPurchaseCallback& callback) { - auto* iap = - [[InAppPurchase alloc] initWithCallback:callback quantity:quantity]; + InAppPurchaseCallback callback) { + auto* iap = [[InAppPurchase alloc] initWithCallback:std::move(callback) + quantity:quantity]; [iap purchaseProduct:base::SysUTF8ToNSString(productID)]; } diff --git a/atom/browser/mac/in_app_purchase_product.h b/atom/browser/mac/in_app_purchase_product.h index 92a880845275..3ce93a0019fa 100644 --- a/atom/browser/mac/in_app_purchase_product.h +++ b/atom/browser/mac/in_app_purchase_product.h @@ -38,13 +38,13 @@ struct Product { // --------------------------- Typedefs --------------------------- -typedef base::Callback&)> +typedef base::OnceCallback)> InAppPurchaseProductsCallback; // --------------------------- Functions --------------------------- void GetProducts(const std::vector& productIDs, - const InAppPurchaseProductsCallback& callback); + InAppPurchaseProductsCallback callback); } // namespace in_app_purchase diff --git a/atom/browser/mac/in_app_purchase_product.mm b/atom/browser/mac/in_app_purchase_product.mm index 77ed389556e5..36745dc188db 100644 --- a/atom/browser/mac/in_app_purchase_product.mm +++ b/atom/browser/mac/in_app_purchase_product.mm @@ -23,8 +23,7 @@ in_app_purchase::InAppPurchaseProductsCallback callback_; } -- (id)initWithCallback: - (const in_app_purchase::InAppPurchaseProductsCallback&)callback; +- (id)initWithCallback:(in_app_purchase::InAppPurchaseProductsCallback)callback; @end @@ -38,9 +37,9 @@ * @param callback - The callback that will be called to return the products. */ - (id)initWithCallback: - (const in_app_purchase::InAppPurchaseProductsCallback&)callback { + (in_app_purchase::InAppPurchaseProductsCallback)callback { if ((self = [super init])) { - callback_ = callback; + callback_ = std::move(callback); } return self; @@ -81,7 +80,7 @@ // Send the callback to the browser thread. base::PostTaskWithTraits(FROM_HERE, {content::BrowserThread::UI}, - base::Bind(callback_, converted)); + base::BindOnce(std::move(callback_), converted)); [self release]; } @@ -167,8 +166,9 @@ Product::Product(const Product&) = default; Product::~Product() = default; void GetProducts(const std::vector& productIDs, - const InAppPurchaseProductsCallback& callback) { - auto* iapProduct = [[InAppPurchaseProduct alloc] initWithCallback:callback]; + InAppPurchaseProductsCallback callback) { + auto* iapProduct = + [[InAppPurchaseProduct alloc] initWithCallback:std::move(callback)]; // Convert the products' id to NSSet. NSMutableSet* productsIDSet = diff --git a/docs/api/in-app-purchase.md b/docs/api/in-app-purchase.md index 6e612a72e271..0434f1885920 100644 --- a/docs/api/in-app-purchase.md +++ b/docs/api/in-app-purchase.md @@ -21,13 +21,23 @@ Returns: The `inAppPurchase` module has the following methods: - ### `inAppPurchase.purchaseProduct(productID, quantity, callback)` * `productID` String - The identifiers of the product to purchase. (The identifier 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. - * `isProductValid` Boolean - Determine if the product is valid and added to the payment queue. + * `isProductValid` Boolean - Determine if the product is valid and added to the payment queue. + +You should listen for the `transactions-updated` event as soon as possible and certainly before you call `purchaseProduct`. + +**[Deprecated Soon](promisification.md)** + +### `inAppPurchase.purchaseProduct(productID, quantity)` + +* `productID` String - The identifiers of the product to purchase. (The identifier of `com.example.app.product1` is `product1`). +* `quantity` Integer (optional) - The number of items the user wants to purchase. + +Returns `Promise` - Returns `true` if the product is valid and added to the payment queue. You should listen for the `transactions-updated` event as soon as possible and certainly before you call `purchaseProduct`. @@ -35,7 +45,17 @@ You should listen for the `transactions-updated` event as soon as possible and c * `productIDs` String[] - The identifiers of the products to get. * `callback` Function - The callback called with the products or an empty array if the products don't exist. - * `products` Product[] - Array of [`Product`](structures/product.md) objects + * `products` Product[] - Array of [`Product`](structures/product.md) objects + +Retrieves the product descriptions. + +**[Deprecated Soon](promisification.md)** + +### `inAppPurchase.getProducts(productIDs)` + +* `productIDs` String[] - The identifiers of the products to get. + +Returns `Promise` - Resolves with an array of [`Product`](structures/product.md) objects. Retrieves the product descriptions. @@ -47,12 +67,10 @@ Returns `Boolean`, whether a user can make a payment. Returns `String`, the path to the receipt. - ### `inAppPurchase.finishAllTransactions()` Completes all pending transactions. - ### `inAppPurchase.finishTransactionByDate(date)` * `date` String - The ISO formatted date of the transaction to finish. diff --git a/docs/api/promisification.md b/docs/api/promisification.md index 9838b378f123..b00edac6854d 100644 --- a/docs/api/promisification.md +++ b/docs/api/promisification.md @@ -11,8 +11,6 @@ When a majority of affected functions are migrated, this flag will be enabled by - [app.importCertificate(options, callback)](https://github.com/electron/electron/blob/master/docs/api/app.md#importCertificate) - [dialog.showMessageBox([browserWindow, ]options[, callback])](https://github.com/electron/electron/blob/master/docs/api/dialog.md#showMessageBox) - [dialog.showCertificateTrustDialog([browserWindow, ]options, callback)](https://github.com/electron/electron/blob/master/docs/api/dialog.md#showCertificateTrustDialog) -- [inAppPurchase.purchaseProduct(productID, quantity, callback)](https://github.com/electron/electron/blob/master/docs/api/in-app-purchase.md#purchaseProduct) -- [inAppPurchase.getProducts(productIDs, callback)](https://github.com/electron/electron/blob/master/docs/api/in-app-purchase.md#getProducts) - [ses.getBlobData(identifier, callback)](https://github.com/electron/electron/blob/master/docs/api/session.md#getBlobData) - [contents.executeJavaScript(code[, userGesture, callback])](https://github.com/electron/electron/blob/master/docs/api/web-contents.md#executeJavaScript) - [contents.print([options], [callback])](https://github.com/electron/electron/blob/master/docs/api/web-contents.md#print) @@ -38,6 +36,8 @@ When a majority of affected functions are migrated, this flag will be enabled by - [desktopCapturer.getSources(options, callback)](https://github.com/electron/electron/blob/master/docs/api/desktop-capturer.md#getSources) - [dialog.showOpenDialog([browserWindow, ]options[, callback])](https://github.com/electron/electron/blob/master/docs/api/dialog.md#showOpenDialog) - [dialog.showSaveDialog([browserWindow, ]options[, callback])](https://github.com/electron/electron/blob/master/docs/api/dialog.md#showSaveDialog) +- [inAppPurchase.purchaseProduct(productID, quantity, callback)](https://github.com/electron/electron/blob/master/docs/api/in-app-purchase.md#purchaseProduct) +- [inAppPurchase.getProducts(productIDs, callback)](https://github.com/electron/electron/blob/master/docs/api/in-app-purchase.md#getProducts) - [netLog.stopLogging([callback])](https://github.com/electron/electron/blob/master/docs/api/net-log.md#stopLogging) - [protocol.isProtocolHandled(scheme, callback)](https://github.com/electron/electron/blob/master/docs/api/protocol.md#isProtocolHandled) - [ses.clearHostResolverCache([callback])](https://github.com/electron/electron/blob/master/docs/api/session.md#clearHostResolverCache) diff --git a/docs/tutorial/in-app-purchases.md b/docs/tutorial/in-app-purchases.md index 6e3c9afe039f..fbd2acb9f3bb 100644 --- a/docs/tutorial/in-app-purchases.md +++ b/docs/tutorial/in-app-purchases.md @@ -3,7 +3,7 @@ ## Preparing ### Paid Applications Agreement -If you haven't already, you’ll need to sign the Paid Applications Agreement and set up your banking and tax information in iTunes Connect. +If you haven't already, you’ll need to sign the Paid Applications Agreement and set up your banking and tax information in iTunes Connect. [iTunes Connect Developer Help: Agreements, tax, and banking overview](https://help.apple.com/itunes-connect/developer/#/devb6df5ee51) @@ -12,7 +12,6 @@ Then, you'll need to configure your in-app purchases in iTunes Connect, and incl [iTunes Connect Developer Help: Create an in-app purchase](https://help.apple.com/itunes-connect/developer/#/devae49fb316) - ### Change the CFBundleIdentifier To test In-App Purchase in development with Electron you'll have to change the `CFBundleIdentifier` in `node_modules/electron/dist/Electron.app/Contents/Info.plist`. You have to replace `com.github.electron` by the bundle identifier of the application you created with iTunes Connect. @@ -22,12 +21,10 @@ To test In-App Purchase in development with Electron you'll have to change the ` com.example.app ``` - ## Code example Here is an example that shows how to use In-App Purchases in Electron. You'll have to replace the product ids by the identifiers of the products created with iTunes Connect (the identifier of `com.example.app.product1` is `product1`). Note that you have to listen to the `transactions-updated` event as soon as possible in your app. - ```javascript const { inAppPurchase } = require('electron').remote const PRODUCT_IDS = ['id1', 'id2'] @@ -95,7 +92,7 @@ if (!inAppPurchase.canMakePayments()) { } // Retrieve and display the product descriptions. -inAppPurchase.getProducts(PRODUCT_IDS, (products) => { +inAppPurchase.getProducts(PRODUCT_IDS).then(products => { // Check the parameters. if (!Array.isArray(products) || products.length <= 0) { console.log('Unable to retrieve the product informations.') @@ -103,17 +100,16 @@ inAppPurchase.getProducts(PRODUCT_IDS, (products) => { } // Display the name and price of each product. - products.forEach((product) => { + products.forEach(product => { console.log(`The price of ${product.localizedTitle} is ${product.formattedPrice}.`) }) // Ask the user which product he/she wants to purchase. - // ... let selectedProduct = products[0] let selectedQuantity = 1 // Purchase the selected product. - inAppPurchase.purchaseProduct(selectedProduct.productIdentifier, selectedQuantity, (isProductValid) => { + inAppPurchase.purchaseProduct(selectedProduct.productIdentifier, selectedQuantity).then(isProductValid => { if (!isProductValid) { console.log('The product is not valid.') return diff --git a/lib/browser/api/in-app-purchase.js b/lib/browser/api/in-app-purchase.js index 95f45892a027..6cbecfd21a3d 100644 --- a/lib/browser/api/in-app-purchase.js +++ b/lib/browser/api/in-app-purchase.js @@ -1,5 +1,7 @@ 'use strict' +const { deprecate } = require('electron') + if (process.platform === 'darwin') { const { EventEmitter } = require('events') const { inAppPurchase, InAppPurchase } = process.atomBinding('in_app_purchase') @@ -18,3 +20,6 @@ if (process.platform === 'darwin') { getReceiptURL: () => '' } } + +module.exports.purchaseProduct = deprecate.promisify(module.exports.purchaseProduct) +module.exports.getProducts = deprecate.promisify(module.exports.getProducts) diff --git a/spec/api-in-app-purchase-spec.js b/spec/api-in-app-purchase-spec.js index 83f673865ef6..a81f28a9174f 100644 --- a/spec/api-in-app-purchase-spec.js +++ b/spec/api-in-app-purchase-spec.js @@ -38,21 +38,39 @@ describe('inAppPurchase module', function () { expect(correctUrlEnd).to.be.true() }) - it('purchaseProduct() fails when buying invalid product', done => { + it('purchaseProduct() fails when buying invalid product', async () => { + const success = await inAppPurchase.purchaseProduct('non-exist', 1) + expect(success).to.be.false() + }) + + // TODO(codebytere): remove when promisification is complete + it('purchaseProduct() fails when buying invalid product (callback)', done => { inAppPurchase.purchaseProduct('non-exist', 1, success => { expect(success).to.be.false() done() }) }) - it('purchaseProduct() accepts optional arguments', done => { - inAppPurchase.purchaseProduct('non-exist', () => { - inAppPurchase.purchaseProduct('non-exist', 1) + it('purchaseProduct() accepts optional arguments', async () => { + const success = await inAppPurchase.purchaseProduct('non-exist') + expect(success).to.be.false() + }) + + // TODO(codebytere): remove when promisification is complete + it('purchaseProduct() accepts optional arguments (callback)', done => { + inAppPurchase.purchaseProduct('non-exist', success => { + expect(success).to.be.false() done() }) }) - it('getProducts() returns an empty list when getting invalid product', done => { + it('getProducts() returns an empty list when getting invalid product', async () => { + const products = await inAppPurchase.getProducts(['non-exist']) + expect(products).to.be.an('array').of.length(0) + }) + + // TODO(codebytere): remove when promisification is complete + it('getProducts() returns an empty list when getting invalid product (callback)', done => { inAppPurchase.getProducts(['non-exist'], products => { expect(products).to.be.an('array').of.length(0) done()