Browse Source

feat: promisify In-App Purchase (#17355)

* feat: promisify In-App Purchase

* use mate::Arguments in GetProducts
Shelley Vohr 6 years ago
parent
commit
3e5a98b5f4

+ 30 - 6
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<v8::Promise> InAppPurchase::PurchaseProduct(
+    const std::string& product_id,
+    mate::Arguments* args) {
+  v8::Isolate* isolate = args->isolate();
+  atom::util::Promise promise(isolate);
+  v8::Local<v8::Promise> 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<bool>,
+                     std::move(promise)));
+
+  return handle;
+}
+
+v8::Local<v8::Promise> InAppPurchase::GetProducts(
+    const std::vector<std::string>& productIDs,
+    mate::Arguments* args) {
+  v8::Isolate* isolate = args->isolate();
+  atom::util::Promise promise(isolate);
+  v8::Local<v8::Promise> handle = promise.GetHandle();
+
+  in_app_purchase::GetProducts(
+      productIDs, base::BindOnce(atom::util::Promise::ResolvePromise<
+                                     std::vector<in_app_purchase::Product>>,
+                                 std::move(promise)));
+
+  return handle;
 }
 
 void InAppPurchase::OnTransactionsUpdated(

+ 6 - 1
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<InAppPurchase>,
   explicit InAppPurchase(v8::Isolate* isolate);
   ~InAppPurchase() override;
 
-  void PurchaseProduct(const std::string& product_id, mate::Arguments* args);
+  v8::Local<v8::Promise> PurchaseProduct(const std::string& product_id,
+                                         mate::Arguments* args);
+
+  v8::Local<v8::Promise> GetProducts(const std::vector<std::string>& productIDs,
+                                     mate::Arguments* args);
 
   // TransactionObserver:
   void OnTransactionsUpdated(

+ 2 - 2
atom/browser/mac/in_app_purchase.h

@@ -13,7 +13,7 @@ namespace in_app_purchase {
 
 // --------------------------- Typedefs ---------------------------
 
-typedef base::Callback<void(bool isProductValid)> InAppPurchaseCallback;
+typedef base::OnceCallback<void(bool isProductValid)> 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
 

+ 9 - 8
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)];
 }

+ 2 - 2
atom/browser/mac/in_app_purchase_product.h

@@ -38,13 +38,13 @@ struct Product {
 
 // --------------------------- Typedefs ---------------------------
 
-typedef base::Callback<void(const std::vector<in_app_purchase::Product>&)>
+typedef base::OnceCallback<void(std::vector<in_app_purchase::Product>)>
     InAppPurchaseProductsCallback;
 
 // --------------------------- Functions ---------------------------
 
 void GetProducts(const std::vector<std::string>& productIDs,
-                 const InAppPurchaseProductsCallback& callback);
+                 InAppPurchaseProductsCallback callback);
 
 }  // namespace in_app_purchase
 

+ 7 - 7
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<std::string>& 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 =

+ 23 - 5
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<Boolean>` - 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<Product[]>` - 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.

+ 2 - 2
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)

+ 4 - 8
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 `
 <string>com.example.app</string>
 ```
 
-
 ## 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

+ 5 - 0
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)

+ 23 - 5
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()