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)
.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,
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(

View file

@ -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(

View file

@ -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

View file

@ -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)];
}

View file

@ -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

View file

@ -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 =

View file

@ -21,7 +21,6 @@ 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`).
@ -31,6 +30,17 @@ The `inAppPurchase` module has the following methods:
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`.
### `inAppPurchase.getProducts(productIDs, callback)`
* `productIDs` String[] - The identifiers of the products to get.
@ -39,6 +49,16 @@ You should listen for the `transactions-updated` event as soon as possible and c
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.
### `inAppPurchase.canMakePayments()`
Returns `Boolean`, whether a user can make a payment.
@ -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.

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)
- [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)

View file

@ -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

View file

@ -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)

View file

@ -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()