@nativescript/payments
Version:
In-App Purchase and Subscriptions for NativeScript
469 lines • 22.5 kB
JavaScript
import { Application, Utils } from '@nativescript/core';
import { PaymentEvent, _payments$ } from './common';
import { Failure } from './failure';
import { Item } from './item';
import { Order, OrderState } from './order';
export { PaymentEvent, paymentEvents, payments$, toMainThread } from './common';
export * from './failure';
export * from './item';
export * from './order';
let _billingClient;
let _isBillingAvailable;
function queryPurchasesParamForSkuType(skuType) {
return com.android.billingclient.api.QueryPurchasesParams.newBuilder().setProductType(skuType).build();
}
export function init() {
if (!_billingClient) {
_payments$.next({
context: PaymentEvent.Context.CONNECTING_STORE,
result: PaymentEvent.Result.STARTED,
payload: null,
});
const context = Utils.android.getApplicationContext();
if (context) {
_billingClient = com.android.billingclient.api.BillingClient.newBuilder(context)
.enablePendingPurchases(com.android.billingclient.api.PendingPurchasesParams.newBuilder().enableOneTimeProducts().build())
.enableAutoServiceReconnection()
.setListener(new com.android.billingclient.api.PurchasesUpdatedListener({
onPurchasesUpdated(result, purchases) {
_purchaseHandler(result.getResponseCode(), purchases);
},
}))
.build();
_payments$.next({
context: PaymentEvent.Context.CONNECTING_STORE,
result: PaymentEvent.Result.PENDING,
payload: null,
});
_billingClient.startConnection(new com.android.billingclient.api.BillingClientStateListener({
onBillingSetupFinished(result) {
const resultCode = result.getResponseCode();
if (_billingClient) {
if (resultCode === com.android.billingclient.api.BillingClient.BillingResponseCode.OK) {
// use this boolean so the user can call `canMakePayments()`
_isBillingAvailable = true;
_billingClient.queryPurchasesAsync(queryPurchasesParamForSkuType(com.android.billingclient.api.BillingClient.SkuType.INAPP), new com.android.billingclient.api.PurchasesResponseListener({
onQueryPurchasesResponse: (historyResult, purchaseList) => {
const responseCode = historyResult.getResponseCode();
_purchaseHandler(responseCode, purchaseList, com.android.billingclient.api.BillingClient.SkuType.INAPP);
_payments$.next({
context: PaymentEvent.Context.CONNECTING_STORE,
result: PaymentEvent.Result.SUCCESS,
payload: null,
});
},
}));
_billingClient.queryPurchasesAsync(queryPurchasesParamForSkuType(com.android.billingclient.api.BillingClient.SkuType.SUBS), new com.android.billingclient.api.PurchasesResponseListener({
onQueryPurchasesResponse: (historyResult, purchaseList) => {
const responseCode = historyResult.getResponseCode();
_purchaseHandler(responseCode, purchaseList, com.android.billingclient.api.BillingClient.SkuType.SUBS);
_payments$.next({
context: PaymentEvent.Context.CONNECTING_STORE,
result: PaymentEvent.Result.SUCCESS,
payload: null,
});
},
}));
}
else {
const code = _mapBillingResponseCode(resultCode);
console.log(new Error(`🛑 In App Billing Response Error Code: ${resultCode} - ${code}`));
if (resultCode === com.android.billingclient.api.BillingClient.BillingResponseCode.BILLING_UNAVAILABLE) {
console.log('The device you are testing on may not have Google Play setup.');
}
// use this boolean so the user can call `canMakePayments()`
_isBillingAvailable = false;
_payments$.next({
context: PaymentEvent.Context.CONNECTING_STORE,
result: PaymentEvent.Result.FAILURE,
payload: new Failure(resultCode),
});
}
}
else {
console.error(new Error('BillingClient missing.'));
}
},
onBillingServiceDisconnected() {
console.log('Billing Service disconnected.');
// .startConnection // TODO Handle retrying connection ?
},
}));
}
else {
console.error(new Error('Application context missing.'));
}
}
}
export function tearDown() {
if (_billingClient) {
_billingClient.endConnection();
}
_billingClient = null;
}
export function fetchItems(itemIds) {
fetchProducts(itemIds, com.android.billingclient.api.BillingClient.SkuType.INAPP);
}
export function fetchSubscriptions(itemIds) {
fetchProducts(itemIds, com.android.billingclient.api.BillingClient.SkuType.SUBS);
}
export function fetchProducts(itemIds, skuType) {
if (_billingClient) {
_payments$.next({
context: PaymentEvent.Context.RETRIEVING_ITEMS,
result: PaymentEvent.Result.STARTED,
payload: itemIds,
});
const skuList = new java.util.ArrayList();
itemIds.forEach((value) => {
return skuList.add(value);
});
// const params = com.android.billingclient.api.SkuDetailsParams.newBuilder();
// params.setSkusList(skuList).setType(skuType);
const details = com.android.billingclient.api.QueryProductDetailsParams.newBuilder()
.setProductList(java.util.Arrays.asList(itemIds.map((id) => {
return com.android.billingclient.api.QueryProductDetailsParams.Product.newBuilder().setProductId(id).setProductType(skuType).build();
})))
.build();
_billingClient.queryProductDetailsAsync(details, new com.android.billingclient.api.ProductDetailsResponseListener({
onProductDetailsResponse(result, detailsResult) {
const responseCode = result.getResponseCode();
if (responseCode === com.android.billingclient.api.BillingClient.BillingResponseCode.OK) {
const detailsList = detailsResult.getProductDetailsList();
const products = [];
const size = detailsList.size();
for (let i = 0; i < size; i++) {
products.push(new Item(detailsList.get(i)));
}
_payments$.next({
context: PaymentEvent.Context.RETRIEVING_ITEMS,
result: PaymentEvent.Result.SUCCESS,
payload: products,
});
}
else {
const code = _mapBillingResponseCode(responseCode);
console.log(new Error(`Failed to fetch products for purchase: ${responseCode} - ${code}`));
_payments$.next({
context: PaymentEvent.Context.RETRIEVING_ITEMS,
result: PaymentEvent.Result.FAILURE,
payload: new Failure(responseCode),
});
}
},
}));
_payments$.next({
context: PaymentEvent.Context.RETRIEVING_ITEMS,
result: PaymentEvent.Result.PENDING,
payload: itemIds,
});
}
else {
console.error(new Error('BillingClient missing.'));
}
}
export function buyItem(item, buyItemOptions) {
startOrder(item, com.android.billingclient.api.BillingClient.SkuType.INAPP, buyItemOptions);
}
export function startSubscription(item, options) {
startOrder(item, com.android.billingclient.api.BillingClient.SkuType.SUBS, options);
}
export function startOrder(item, skuType, options) {
if (_billingClient) {
let pendingCount = 0;
_billingClient.queryPurchasesAsync(queryPurchasesParamForSkuType(skuType), new com.android.billingclient.api.PurchasesResponseListener({
onQueryPurchasesResponse(result, pending) {
// const pending = _billingClient.queryPurchases(skuType).getPurchasesList();
if (pending) {
pendingCount = pending.size();
}
if (!pendingCount) {
_payments$.next({
context: PaymentEvent.Context.PROCESSING_ORDER,
result: PaymentEvent.Result.PENDING,
payload: pendingCount + 1,
});
const paramsBuilder = com.android.billingclient.api.BillingFlowParams.newBuilder();
let details;
if (skuType == com.android.billingclient.api.BillingClient.SkuType.INAPP) {
details = com.android.billingclient.api.BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(item.nativeValue)
.build();
}
else if (skuType == com.android.billingclient.api.BillingClient.SkuType.SUBS) {
details = com.android.billingclient.api.BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(item.nativeValue)
.setOfferToken(item.offerToken)
.build();
}
paramsBuilder.setProductDetailsParamsList(java.util.Arrays.asList([details]));
if (options) {
if (options?.accountUserName) {
paramsBuilder.setObfuscatedProfileId(options.accountUserName);
}
if (options?.android?.obfuscatedProfileId) {
paramsBuilder.setObfuscatedProfileId(options.android.obfuscatedProfileId);
}
if (options?.android?.obfuscatedAccountId) {
paramsBuilder.setObfuscatedAccountId(options.android.obfuscatedAccountId);
}
}
const result = _billingClient.launchBillingFlow(Application.android.foregroundActivity, paramsBuilder.build());
const responseCode = result.getResponseCode();
if (responseCode === com.android.billingclient.api.BillingClient.BillingResponseCode.OK) {
_payments$.next({
context: PaymentEvent.Context.PROCESSING_ORDER,
result: PaymentEvent.Result.STARTED,
payload: item,
});
}
else {
_payments$.next({
context: PaymentEvent.Context.PROCESSING_ORDER,
result: PaymentEvent.Result.FAILURE,
payload: new Failure(responseCode),
});
}
}
else {
_payments$.next({
context: PaymentEvent.Context.PROCESSING_ORDER,
result: PaymentEvent.Result.PENDING,
payload: pendingCount,
});
}
},
}));
}
else {
console.error(new Error('BillingClient missing.'));
}
}
export function finalizeOrder(order, consume) {
if (!_billingClient) {
console.error(new Error('BillingClient missing.'));
return;
}
if (order.isSubscription || !consume) {
if (order.acknowledged) {
_payments$.next({
context: PaymentEvent.Context.FINALIZING_ORDER,
result: PaymentEvent.Result.SUCCESS,
payload: new Order(order.nativeValue, order.restored),
});
return;
}
const params = com.android.billingclient.api.AcknowledgePurchaseParams.newBuilder().setPurchaseToken(order.receiptToken).build();
_billingClient.acknowledgePurchase(params, new com.android.billingclient.api.AcknowledgePurchaseResponseListener({
onAcknowledgePurchaseResponse: (result) => {
if (result.getResponseCode() === com.android.billingclient.api.BillingClient.BillingResponseCode.OK) {
_payments$.next({
context: PaymentEvent.Context.FINALIZING_ORDER,
result: PaymentEvent.Result.SUCCESS,
payload: new Order(order.nativeValue, order.restored),
});
}
else {
_payments$.next({
context: PaymentEvent.Context.FINALIZING_ORDER,
result: PaymentEvent.Result.FAILURE,
payload: new Failure(result.getResponseCode()),
});
}
},
}));
return;
}
_payments$.next({
context: PaymentEvent.Context.FINALIZING_ORDER,
result: PaymentEvent.Result.STARTED,
payload: order,
});
if (order.state === OrderState.VALID && !order.restored) {
const consumeParams = com.android.billingclient.api.ConsumeParams.newBuilder().setPurchaseToken(order.receiptToken).build();
_billingClient.consumeAsync(consumeParams, new com.android.billingclient.api.ConsumeResponseListener({
onConsumeResponse: (result, purchaseToken) => {
const responseCode = result.getResponseCode();
if (_billingClient) {
if (responseCode === com.android.billingclient.api.BillingClient.BillingResponseCode.OK) {
_payments$.next({
context: PaymentEvent.Context.FINALIZING_ORDER,
result: PaymentEvent.Result.SUCCESS,
payload: new Order(order.nativeValue, order.restored),
});
}
else {
_payments$.next({
context: PaymentEvent.Context.FINALIZING_ORDER,
result: PaymentEvent.Result.FAILURE,
payload: new Failure(responseCode),
});
}
_billingClient.queryPurchasesAsync(queryPurchasesParamForSkuType(com.android.billingclient.api.BillingClient.SkuType.INAPP), new com.android.billingclient.api.PurchasesResponseListener({
onQueryPurchasesResponse(param0, pending) {
_payments$.next({
context: PaymentEvent.Context.PROCESSING_ORDER,
result: PaymentEvent.Result.PENDING,
payload: pending ? pending.size() : 0,
});
},
}));
}
else {
console.error(new Error('BillingClient missing.'));
}
},
}));
_payments$.next({
context: PaymentEvent.Context.FINALIZING_ORDER,
result: PaymentEvent.Result.PENDING,
payload: order,
});
}
else {
_payments$.next({
context: PaymentEvent.Context.FINALIZING_ORDER,
result: PaymentEvent.Result.FAILURE,
payload: new Failure(8),
});
}
}
export function restoreOrders(skuType) {
if (_billingClient) {
_payments$.next({
context: PaymentEvent.Context.RESTORING_ORDERS,
result: PaymentEvent.Result.STARTED,
payload: null,
});
if (skuType === 'sub') {
skuType = com.android.billingclient.api.BillingClient.SkuType.SUBS;
}
else {
skuType = com.android.billingclient.api.BillingClient.SkuType.INAPP;
}
_billingClient.queryPurchasesAsync(queryPurchasesParamForSkuType(skuType), new com.android.billingclient.api.PurchasesResponseListener({
onQueryPurchasesResponse: (result, purchasesList) => {
const responseCode = result.getResponseCode();
if (responseCode === com.android.billingclient.api.BillingClient.BillingResponseCode.OK) {
const size = purchasesList.size();
for (let i = 0; i < size; i++) {
const purchase = purchasesList.get(i);
if (purchase) {
_payments$.next({
context: PaymentEvent.Context.PROCESSING_ORDER,
result: PaymentEvent.Result.SUCCESS,
// we no longer restore orders, we just process them (if it shows up here it means that it's either an active subscription or an unconsumed order)
// meaning that it can be acknowledged (if order.acknowledged = false) or consumed (if order.isSubscription = false)
payload: new Order(purchase, false),
});
_payments$.next({
context: PaymentEvent.Context.RESTORING_ORDERS,
result: PaymentEvent.Result.PENDING,
payload: new Order(purchase, false),
});
}
}
_payments$.next({
context: PaymentEvent.Context.RESTORING_ORDERS,
result: PaymentEvent.Result.SUCCESS,
payload: null,
});
}
else {
_payments$.next({
context: PaymentEvent.Context.RESTORING_ORDERS,
result: PaymentEvent.Result.FAILURE,
payload: new Failure(responseCode),
});
}
},
}));
}
else {
console.error(new Error('BillingClient missing.'));
}
}
export function canMakePayments( /*types*/) {
if (_billingClient) {
return _isBillingAvailable;
}
else {
console.log('🛑 Call `init` prior to checking if the payments are configured correctly. 🛑');
return false;
}
}
function _purchaseHandler(responseCode, purchases, skuType) {
if (_billingClient) {
const pending = purchases;
if (!skuType) {
_billingClient.queryPurchasesAsync(queryPurchasesParamForSkuType(com.android.billingclient.api.BillingClient.SkuType.INAPP), new com.android.billingclient.api.PurchasesResponseListener({
onQueryPurchasesResponse(param0, pending) {
if (responseCode === com.android.billingclient.api.BillingClient.BillingResponseCode.OK) {
const size = purchases?.size?.() ?? 0;
if (purchases && size) {
for (let i = 0; i < size; i++) {
const purchase = purchases.get(i);
if (purchase) {
const order = new Order(purchase, false);
// order.isSubscription = isSubscription;
_payments$.next({
context: PaymentEvent.Context.PROCESSING_ORDER,
result: PaymentEvent.Result.SUCCESS,
payload: order,
});
}
}
}
}
else {
_payments$.next({
context: PaymentEvent.Context.PROCESSING_ORDER,
result: PaymentEvent.Result.FAILURE,
payload: new Failure(responseCode),
});
}
},
}));
}
// var isSubscription = skuType === com.android.billingclient.api.BillingClient.SkuType.SUBS;
_payments$.next({
context: PaymentEvent.Context.PROCESSING_ORDER,
result: PaymentEvent.Result.PENDING,
payload: pending ? pending.size() : 0,
});
}
else {
console.error(new Error('BillingClient missing.'));
}
}
function _mapBillingResponseCode(code) {
switch (code) {
case 0:
return 'OK';
case 1:
return 'USER_CANCELED';
case 2:
return 'SERVICE_UNAVAILABLE';
case 3:
return 'BILLING_UNAVAILABLE';
case 4:
return 'ITEM_UNAVAILABLE';
case 5:
return 'DEVELOPER_ERROR';
case 6:
return 'ERROR';
case 7:
return 'ITEM_ALREADY_OWNED';
case 8:
return 'ITEM_NOT_OWNED';
case -1:
return 'SERVICE_DISCONNECTED';
case -2:
return 'FEATURE_NOT_SUPPORTED';
case -3:
return 'SERVICE_TIMEOUT';
default:
return '';
}
}
//# sourceMappingURL=index.android.js.map