feat: promisify In-App Purchase (#17355)

* feat: promisify In-App Purchase

* use mate::Arguments in GetProducts
This commit is contained in:
Shelley Vohr 2019-03-13 13:56:01 -07:00 committed by GitHub
parent faabd0cc8b
commit 3e5a98b5f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 113 additions and 46 deletions

View file

@ -91,7 +91,7 @@ void InAppPurchase::BuildPrototype(v8::Isolate* isolate,
&in_app_purchase::FinishAllTransactions) &in_app_purchase::FinishAllTransactions)
.SetMethod("finishTransactionByDate", .SetMethod("finishTransactionByDate",
&in_app_purchase::FinishTransactionByDate) &in_app_purchase::FinishTransactionByDate)
.SetMethod("getProducts", &in_app_purchase::GetProducts); .SetMethod("getProducts", &InAppPurchase::GetProducts);
} }
InAppPurchase::InAppPurchase(v8::Isolate* isolate) { InAppPurchase::InAppPurchase(v8::Isolate* isolate) {
@ -100,13 +100,37 @@ InAppPurchase::InAppPurchase(v8::Isolate* isolate) {
InAppPurchase::~InAppPurchase() {} InAppPurchase::~InAppPurchase() {}
void InAppPurchase::PurchaseProduct(const std::string& product_id, v8::Local<v8::Promise> InAppPurchase::PurchaseProduct(
mate::Arguments* args) { 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; int quantity = 1;
in_app_purchase::InAppPurchaseCallback callback;
args->GetNext(&quantity); 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( void InAppPurchase::OnTransactionsUpdated(

View file

@ -12,6 +12,7 @@
#include "atom/browser/mac/in_app_purchase.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_observer.h"
#include "atom/browser/mac/in_app_purchase_product.h" #include "atom/browser/mac/in_app_purchase_product.h"
#include "atom/common/promise_util.h"
#include "native_mate/handle.h" #include "native_mate/handle.h"
namespace atom { namespace atom {
@ -30,7 +31,11 @@ class InAppPurchase : public mate::EventEmitter<InAppPurchase>,
explicit InAppPurchase(v8::Isolate* isolate); explicit InAppPurchase(v8::Isolate* isolate);
~InAppPurchase() override; ~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: // TransactionObserver:
void OnTransactionsUpdated( void OnTransactionsUpdated(

View file

@ -13,7 +13,7 @@ namespace in_app_purchase {
// --------------------------- Typedefs --------------------------- // --------------------------- Typedefs ---------------------------
typedef base::Callback<void(bool isProductValid)> InAppPurchaseCallback; typedef base::OnceCallback<void(bool isProductValid)> InAppPurchaseCallback;
// --------------------------- Functions --------------------------- // --------------------------- Functions ---------------------------
@ -27,7 +27,7 @@ std::string GetReceiptURL(void);
void PurchaseProduct(const std::string& productID, void PurchaseProduct(const std::string& productID,
int quantity, int quantity,
const InAppPurchaseCallback& callback); InAppPurchaseCallback callback);
} // namespace in_app_purchase } // namespace in_app_purchase

View file

@ -25,7 +25,7 @@
NSInteger quantity_; NSInteger quantity_;
} }
- (id)initWithCallback:(const in_app_purchase::InAppPurchaseCallback&)callback - (id)initWithCallback:(in_app_purchase::InAppPurchaseCallback)callback
quantity:(NSInteger)quantity; quantity:(NSInteger)quantity;
- (void)purchaseProduct:(NSString*)productID; - (void)purchaseProduct:(NSString*)productID;
@ -42,10 +42,10 @@
* @param callback - The callback that will be called when the payment is added * @param callback - The callback that will be called when the payment is added
* to the queue. * to the queue.
*/ */
- (id)initWithCallback:(const in_app_purchase::InAppPurchaseCallback&)callback - (id)initWithCallback:(in_app_purchase::InAppPurchaseCallback)callback
quantity:(NSInteger)quantity { quantity:(NSInteger)quantity {
if ((self = [super init])) { if ((self = [super init])) {
callback_ = callback; callback_ = std::move(callback);
quantity_ = quantity; quantity_ = quantity;
} }
@ -119,8 +119,9 @@
*/ */
- (void)runCallback:(bool)isProductValid { - (void)runCallback:(bool)isProductValid {
if (callback_) { if (callback_) {
base::PostTaskWithTraits(FROM_HERE, {content::BrowserThread::UI}, base::PostTaskWithTraits(
base::Bind(callback_, isProductValid)); FROM_HERE, {content::BrowserThread::UI},
base::BindOnce(std::move(callback_), isProductValid));
} }
// Release this delegate. // Release this delegate.
[self release]; [self release];
@ -177,9 +178,9 @@ std::string GetReceiptURL() {
void PurchaseProduct(const std::string& productID, void PurchaseProduct(const std::string& productID,
int quantity, int quantity,
const InAppPurchaseCallback& callback) { InAppPurchaseCallback callback) {
auto* iap = auto* iap = [[InAppPurchase alloc] initWithCallback:std::move(callback)
[[InAppPurchase alloc] initWithCallback:callback quantity:quantity]; quantity:quantity];
[iap purchaseProduct:base::SysUTF8ToNSString(productID)]; [iap purchaseProduct:base::SysUTF8ToNSString(productID)];
} }

View file

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

View file

@ -23,8 +23,7 @@
in_app_purchase::InAppPurchaseProductsCallback callback_; in_app_purchase::InAppPurchaseProductsCallback callback_;
} }
- (id)initWithCallback: - (id)initWithCallback:(in_app_purchase::InAppPurchaseProductsCallback)callback;
(const in_app_purchase::InAppPurchaseProductsCallback&)callback;
@end @end
@ -38,9 +37,9 @@
* @param callback - The callback that will be called to return the products. * @param callback - The callback that will be called to return the products.
*/ */
- (id)initWithCallback: - (id)initWithCallback:
(const in_app_purchase::InAppPurchaseProductsCallback&)callback { (in_app_purchase::InAppPurchaseProductsCallback)callback {
if ((self = [super init])) { if ((self = [super init])) {
callback_ = callback; callback_ = std::move(callback);
} }
return self; return self;
@ -81,7 +80,7 @@
// Send the callback to the browser thread. // Send the callback to the browser thread.
base::PostTaskWithTraits(FROM_HERE, {content::BrowserThread::UI}, base::PostTaskWithTraits(FROM_HERE, {content::BrowserThread::UI},
base::Bind(callback_, converted)); base::BindOnce(std::move(callback_), converted));
[self release]; [self release];
} }
@ -167,8 +166,9 @@ Product::Product(const Product&) = default;
Product::~Product() = default; Product::~Product() = default;
void GetProducts(const std::vector<std::string>& productIDs, void GetProducts(const std::vector<std::string>& productIDs,
const InAppPurchaseProductsCallback& callback) { InAppPurchaseProductsCallback callback) {
auto* iapProduct = [[InAppPurchaseProduct alloc] initWithCallback:callback]; auto* iapProduct =
[[InAppPurchaseProduct alloc] initWithCallback:std::move(callback)];
// Convert the products' id to NSSet. // Convert the products' id to NSSet.
NSMutableSet* productsIDSet = NSMutableSet* productsIDSet =

View file

@ -21,13 +21,23 @@ Returns:
The `inAppPurchase` module has the following methods: The `inAppPurchase` module has the following methods:
### `inAppPurchase.purchaseProduct(productID, quantity, callback)` ### `inAppPurchase.purchaseProduct(productID, quantity, callback)`
* `productID` String - The identifiers of the product to purchase. (The identifier of `com.example.app.product1` is `product1`). * `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. * `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. * `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`. 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. * `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. * `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. Retrieves the product descriptions.
@ -47,12 +67,10 @@ Returns `Boolean`, whether a user can make a payment.
Returns `String`, the path to the receipt. Returns `String`, the path to the receipt.
### `inAppPurchase.finishAllTransactions()` ### `inAppPurchase.finishAllTransactions()`
Completes all pending transactions. Completes all pending transactions.
### `inAppPurchase.finishTransactionByDate(date)` ### `inAppPurchase.finishTransactionByDate(date)`
* `date` String - The ISO formatted date of the transaction to finish. * `date` String - The ISO formatted date of the transaction to finish.

View file

@ -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) - [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.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) - [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) - [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.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) - [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) - [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.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) - [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) - [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) - [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) - [ses.clearHostResolverCache([callback])](https://github.com/electron/electron/blob/master/docs/api/session.md#clearHostResolverCache)

View file

@ -3,7 +3,7 @@
## Preparing ## Preparing
### Paid Applications Agreement ### Paid Applications Agreement
If you haven't already, youll need to sign the Paid Applications Agreement and set up your banking and tax information in iTunes Connect. If you haven't already, youll 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) [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) [iTunes Connect Developer Help: Create an in-app purchase](https://help.apple.com/itunes-connect/developer/#/devae49fb316)
### Change the CFBundleIdentifier ### 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. 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> <string>com.example.app</string>
``` ```
## Code example ## 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. 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 ```javascript
const { inAppPurchase } = require('electron').remote const { inAppPurchase } = require('electron').remote
const PRODUCT_IDS = ['id1', 'id2'] const PRODUCT_IDS = ['id1', 'id2']
@ -95,7 +92,7 @@ if (!inAppPurchase.canMakePayments()) {
} }
// Retrieve and display the product descriptions. // Retrieve and display the product descriptions.
inAppPurchase.getProducts(PRODUCT_IDS, (products) => { inAppPurchase.getProducts(PRODUCT_IDS).then(products => {
// Check the parameters. // Check the parameters.
if (!Array.isArray(products) || products.length <= 0) { if (!Array.isArray(products) || products.length <= 0) {
console.log('Unable to retrieve the product informations.') 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. // Display the name and price of each product.
products.forEach((product) => { products.forEach(product => {
console.log(`The price of ${product.localizedTitle} is ${product.formattedPrice}.`) console.log(`The price of ${product.localizedTitle} is ${product.formattedPrice}.`)
}) })
// Ask the user which product he/she wants to purchase. // Ask the user which product he/she wants to purchase.
// ...
let selectedProduct = products[0] let selectedProduct = products[0]
let selectedQuantity = 1 let selectedQuantity = 1
// Purchase the selected product. // Purchase the selected product.
inAppPurchase.purchaseProduct(selectedProduct.productIdentifier, selectedQuantity, (isProductValid) => { inAppPurchase.purchaseProduct(selectedProduct.productIdentifier, selectedQuantity).then(isProductValid => {
if (!isProductValid) { if (!isProductValid) {
console.log('The product is not valid.') console.log('The product is not valid.')
return return

View file

@ -1,5 +1,7 @@
'use strict' 'use strict'
const { deprecate } = require('electron')
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
const { EventEmitter } = require('events') const { EventEmitter } = require('events')
const { inAppPurchase, InAppPurchase } = process.atomBinding('in_app_purchase') const { inAppPurchase, InAppPurchase } = process.atomBinding('in_app_purchase')
@ -18,3 +20,6 @@ if (process.platform === 'darwin') {
getReceiptURL: () => '' getReceiptURL: () => ''
} }
} }
module.exports.purchaseProduct = deprecate.promisify(module.exports.purchaseProduct)
module.exports.getProducts = deprecate.promisify(module.exports.getProducts)

View file

@ -38,21 +38,39 @@ describe('inAppPurchase module', function () {
expect(correctUrlEnd).to.be.true() 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 => { inAppPurchase.purchaseProduct('non-exist', 1, success => {
expect(success).to.be.false() expect(success).to.be.false()
done() done()
}) })
}) })
it('purchaseProduct() accepts optional arguments', done => { it('purchaseProduct() accepts optional arguments', async () => {
inAppPurchase.purchaseProduct('non-exist', () => { const success = await inAppPurchase.purchaseProduct('non-exist')
inAppPurchase.purchaseProduct('non-exist', 1) 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() 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 => { inAppPurchase.getProducts(['non-exist'], products => {
expect(products).to.be.an('array').of.length(0) expect(products).to.be.an('array').of.length(0)
done() done()