Skip to content

Instantly share code, notes, and snippets.

@j3k0
Created June 28, 2023 11:50
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save j3k0/ff931dbb14887729c860873e3bbc7dc5 to your computer and use it in GitHub Desktop.
Save j3k0/ff931dbb14887729c860873e3bbc7dc5 to your computer and use it in GitHub Desktop.
import 'cordova-plugin-purchase';
document.addEventListener('deviceready', onDeviceReady);
let log: CdvPurchase.Logger;
let statusMessage: null | string = null;
function setStatusMessage(value: null | string) {
if (statusMessage !== value) {
statusMessage = value;
renderStatusUI();
}
}
function onDeviceReady() {
const store = CdvPurchase.store;
log = new CdvPurchase.Logger({ verbosity: CdvPurchase.LogLevel.DEBUG }, 'MicroExample');
log.info('onDeviceRead()');
const { ProductType, Platform, LogLevel, Product, VerifiedReceipt } = CdvPurchase;
const products: CdvPurchase.IRegisterProduct[] = [{
id: 'demo_monthly_basic',
type: ProductType.PAID_SUBSCRIPTION,
platform: Platform.GOOGLE_PLAY,
}, {
id: 'subscription1',
type: ProductType.PAID_SUBSCRIPTION,
platform: Platform.APPLE_APPSTORE,
}, {
id: 'cc.fovea.purchase.subscription1sx',
type: ProductType.PAID_SUBSCRIPTION,
platform: Platform.APPLE_APPSTORE,
}, {
id: 'subscription2',
type: ProductType.PAID_SUBSCRIPTION,
platform: Platform.APPLE_APPSTORE,
}, {
id: 'cc.fovea.purchase.subscription2sx',
type: ProductType.PAID_SUBSCRIPTION,
platform: Platform.APPLE_APPSTORE,
}, {
id: '1_token',
type: ProductType.CONSUMABLE,
platform: Platform.GOOGLE_PLAY,
}, {
id: '1_token',
type: ProductType.CONSUMABLE,
platform: Platform.APPLE_APPSTORE,
},
{
id: 'demo_weekly_basic',
type: ProductType.PAID_SUBSCRIPTION,
platform: Platform.GOOGLE_PLAY,
}, {
id: 'subscription1',
type: ProductType.PAID_SUBSCRIPTION,
platform: Platform.GOOGLE_PLAY,
}, {
id: 'subscription2',
type: ProductType.PAID_SUBSCRIPTION,
platform: Platform.GOOGLE_PLAY,
}, {
id: 'cc.fovea.purchase.subscription1sx',
type: ProductType.PAID_SUBSCRIPTION,
platform: Platform.GOOGLE_PLAY,
}, {
id: 'cc.fovea.purchase.subscription2sx',
type: ProductType.PAID_SUBSCRIPTION,
platform: Platform.GOOGLE_PLAY,
}, {
id: '1_token',
type: ProductType.CONSUMABLE,
platform: Platform.GOOGLE_PLAY,
}, {
id: 'consumable1',
type: ProductType.CONSUMABLE,
platform: Platform.GOOGLE_PLAY,
}, {
id: 'consumable2',
type: ProductType.CONSUMABLE,
platform: Platform.GOOGLE_PLAY,
}, {
id: 'cc.fovea.purchase.nonrenewing.1hour',
type: ProductType.NON_RENEWING_SUBSCRIPTION,
platform: Platform.GOOGLE_PLAY,
}, {
id: 'cc.fovea.purchase.nonrenewing.5minutes',
type: ProductType.NON_RENEWING_SUBSCRIPTION,
platform: Platform.GOOGLE_PLAY,
}, {
id: 'cc.fovea.purchase.subscription2',
type: ProductType.PAID_SUBSCRIPTION,
platform: Platform.GOOGLE_PLAY,
}, {
id: 'cc.fovea.purchase.consumable1',
type: ProductType.CONSUMABLE,
platform: Platform.GOOGLE_PLAY,
}, {
id: 'cc.fovea.purchase.nonconsumable1',
type: ProductType.NON_CONSUMABLE,
platform: Platform.GOOGLE_PLAY,
}, {
id: 'cc.fovea.purchase.subscription1',
type: ProductType.PAID_SUBSCRIPTION,
platform: Platform.GOOGLE_PLAY,
},
CdvPurchase.Test.testProducts.PAID_SUBSCRIPTION_ACTIVE,
CdvPurchase.Test.testProducts.CONSUMABLE,
CdvPurchase.Test.testProducts.CONSUMABLE_FAILING,
];
// We should first register all our products or we cannot use them in the app.
store.register(products);
store.verbosity = LogLevel.DEBUG;
store.applicationUsername = () => "my_username_3"; // the plugin will hash this with md5 where needed
// Show errors on the dedicated Div.
store.error(errorHandler);
// Define events handler for our subscription products
store.when()
.productUpdated(product => {
// Re-render the interface on updates
log.info('Product updated: ' + JSON.stringify(product));
renderUI();
})
.receiptUpdated(receipt => {
// Re-render the interface on updates
log.info('Receipt updated.');
// log.info('Receipt updated: ' + JSON.stringify(receipt));
renderUI();
})
.approved(transaction => {
log.info('Transaction approved: ' + JSON.stringify(transaction));
// verify approved transactions
transaction.verify();
})
.verified(receipt => {
// finish transactions from verified receipts
log.info('Receipt verified');
receipt.finish();
renderUI();
})
.unverified(receipt => {
// finish transactions from verified receipts
log.info('Receipt unverified');
})
.finished(transaction => {
log.info('Transaction finished');
renderUI();
})
.receiptsReady(() => {
log.info('Receipts ready');
})
.receiptsVerified(() => {
log.info('Receipts verified');
});
const iaptic = new CdvPurchase.Iaptic({
appName: 'test.app',
apiKey: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx'
});
// For subscriptions and secured transactions, we setup a receipt validator.
store.validator = iaptic.validator;
store.validator_privacy_policy = ['analytics', 'support', 'tracking', 'fraud'];
// Load informations about products and purchases
log.info('calling store initialize()');
store.initialize([
{ platform: Platform.TEST },
{ platform: Platform.GOOGLE_PLAY },
{
platform: Platform.APPLE_APPSTORE,
options: {
needAppReceipt: true,
discountEligibilityDeterminer: iaptic.appStoreDiscountEligibilityDeterminer,
}
},
{
platform: Platform.BRAINTREE,
options: {
clientTokenProvider: iaptic.braintreeClientTokenProvider,
threeDSecure: {
threeDSecureRequest: {
exemptionRequested: true,
}
},
googlePay: {
googleMerchantName: 'Iaptic',
countryCode: 'FR',
environment: 'TEST',
}
}
}
] as CdvPurchase.PlatformWithOptions[]);
// Updates the user interface to reflect the initial state
renderUI();
}
function renderStatusUI() {
// When any of our subscription products is owned, display "Subscribed".
// If one of them is being purchased or validated, display "Processing".
// In all other cases, display "Not Subscribed".
const statusElement = document.getElementById('status');
if (!statusElement) return;
const subscriptions = CdvPurchase.store.products.filter(p => p.type === CdvPurchase.ProductType.PAID_SUBSCRIPTION);
const owned = findVerifiedPurchase(subscriptions, p => !p.isExpired) as CdvPurchase.VerifiedPurchase;
if (statusMessage) {
statusElement.innerHTML = `<h3>${statusMessage}</h3>`;
}
else if (owned) {
statusElement.innerHTML = `<h3>Subscribed</h3>`
+ `<div>to: ${owned.id}</div>`
+ (owned.purchaseDate ? `<div>since: ${new Date(owned.purchaseDate).toLocaleString()}</div>` : '')
+ (owned.expiryDate ? `<div>until: ${new Date(owned.expiryDate).toLocaleString()}</div>` : '')
;
}
else if (isApproved(subscriptions) || isInitiated(subscriptions))
statusElement.innerHTML = 'Processing...';
else
statusElement.innerHTML = 'Not Subscribed';
}
function isOwned(products: CdvPurchase.Product[]): boolean {
return products.filter(p => p.owned).length > 0;
}
function isApproved(products: CdvPurchase.Product[]) {
return !!findLocalTransaction(products, t => t.state === CdvPurchase.TransactionState.APPROVED);
}
function isInitiated(products: CdvPurchase.Product[]) {
return !!findLocalTransaction(products, t => t.state === CdvPurchase.TransactionState.INITIATED);
}
// Find a verified purchase for one of the provided products that passes the given filter.
function findVerifiedPurchase(products: CdvPurchase.Product[], filter: (purchase: CdvPurchase.VerifiedPurchase) => boolean): CdvPurchase.VerifiedPurchase | undefined {
for (const product of products) {
const purchase = CdvPurchase.store.findInVerifiedReceipts(product);
if (!purchase) continue;
if (filter(purchase)) return purchase;
}
}
// Find a local transaction for one of the provided products that passes the given filter.
function findLocalTransaction(products: CdvPurchase.Product[], filter: (transaction: CdvPurchase.Transaction) => boolean): CdvPurchase.Transaction | undefined {
// find if some of those products are part of a receipt
for (const product of products) {
const transaction = CdvPurchase.store.findInLocalReceipts(product);
if (!transaction) continue;
if (filter(transaction)) return transaction;
}
}
/**
* Generate HTML for the product offers
*/
const renderOffers = (product: CdvPurchase.Product) => {
return product.offers ? '<div><ul>' + product.offers.map(offer => {
return '<li>' + (offer.pricingPhases || []).map(pricingPhase => {
return `<b>${pricingPhase.price}</b>` + (product.type === CdvPurchase.ProductType.PAID_SUBSCRIPTION ? ` (${CdvPurchase.Utils.formatBillingCycleEN(pricingPhase)})` : '');
}).join('<br/>then ')
// add the "Buy" button that calls `orderOffer`
+ (offer.pricingPhases.length > 1 ? '<br/>' : ' ')
+ (offer.canPurchase ? `<button onclick="orderOffer('${product.platform}', '${product.id}', '${offer.id}')">Buy</button>` : ` (this offer cannot be purchased)`)
+ `</li>`;
}).join('') + '</ul></div>' : '';
}
// Perform a full render of the user interface
function renderUI() {
try {
const store = CdvPurchase.store;
// log.info('Products: ' + JSON.stringify(store.products));
// log.info('Local Receipts: ' + JSON.stringify(store.localReceipts));
renderStatusUI();
const productsElement = document.getElementById('products');
if (!productsElement) {
log.warn('No element with id=products');
return;
}
/**
* Refresh the displayed details about a product in the DOM
*/
const renderProductUI = (product: CdvPurchase.Product) => {
log.debug("renderProductUI: " + product.id);
const productId = product.id;
const el = document.getElementById(`product-${productId}`);
if (!el) {
log.error(`HTML element product-${productId} does not exists`);
return;
}
function strikeIf(when: boolean) { return when ? '<strike>' : ''; }
function strikeEnd(when: boolean) { return when ? '</strike>' : ''; }
// Create and update the HTML content
const title = `<h3>${product.title}</h3>`;
const id = `<div><b>id:</b> ${product.id}</div>`;
const info = (product.description ? `<div>${product.description || ''}</div>` : '');
const offers = renderOffers(product);
const html = title + id + info + /* discounts + subInfo + */ offers;
log.debug('HTML=' + html);
el.innerHTML = html;
}
const validProducts = store.products.filter(product => product.offers.length > 0);
productsElement.innerHTML =
'<h2>Products</h2>'
+ validProducts
.map(product => `<div id="product-${product.id}" style="margin-top: 30px">...</div>`)
.join('')
+ '<h2>Local Transactions</h2>'
+ store.localTransactions.map(tr => `<div id="transaction-${tr.transactionId}">...</div>`).join('')
+ '<h2>Verified Purchases</h2>'
+ store.verifiedPurchases.map(pr => `<div id="purchase-${pr.id}">...</div>`).join('')
// Render the products' DOM elements
validProducts.forEach(renderProductUI);
const renderLocalTransactionUI = (tr: CdvPurchase.Transaction) => {
const el = document.getElementById(`transaction-${tr.transactionId}`);
if (!el) {
log.error(`HTML element transaction-${tr.transactionId} does not exists`);
return;
}
el.innerHTML = `
<h3>Transaction ${tr.transactionId}</h3>
<div>Products: ${tr.products.map(p => p.id).join(', ')}</div>
<div>State: ${tr.state}</div>
${tr.purchaseDate ? `<div>Date: ${tr.purchaseDate?.toLocaleString()}</div>` : ''}
${tr.expirationDate ? `<div>Expiry Date: ${tr.expirationDate.toLocaleString()}</div>` : ''}
${tr.isAcknowledged || tr.isConsumed ? `<div>${tr.isAcknowledged ? 'Acknowledged ' : ''} ${tr.isConsumed ? 'Consumed ' : ''}</div>` : ''}
`;
}
const renderVerifiedPurchaseUI = (pr: CdvPurchase.VerifiedPurchase) => {
const el = document.getElementById(`purchase-${pr.id}`);
if (!el) {
log.error(`HTML element purchase-${pr.id} does not exists`);
return;
}
const tags = [
pr.renewalIntent,
pr.cancelationReason && ('Canceled by ' + pr.cancelationReason),
pr.isBillingRetryPeriod && 'Billing retry period',
].filter(t => t).join(', ');
el.innerHTML = `
<h3>Product: ${pr.id}</h3>
<div>Platform: ${pr.platform}</div>
<div>Date: ${pr.purchaseDate ? new Date(pr.purchaseDate).toLocaleString() : ''}</div>
${pr.expiryDate ? `<div>Expiry Date: ${new Date(pr.expiryDate).toLocaleString()}</div>` : ''}
${tags ? `<div>${tags}</div>` : ''}
`;
}
store.localTransactions.forEach(renderLocalTransactionUI);
store.verifiedPurchases.forEach(renderVerifiedPurchaseUI);
}
catch (err) {
log.error('ERROR: ' + err);
log.error((err as Error).stack);
}
}
function orderOffer(platform: CdvPurchase.Platform, productId: string, offerId: string) {
const store = CdvPurchase.store;
const offer = store.get(productId, platform)?.getOffer(offerId);
if (offer) {
store.order(offer)
.then((result) => {
if (result && result.isError) {
if (result.code === CdvPurchase.ErrorCode.PAYMENT_CANCELLED) {
alert('payment cancelled');
}
else {
alert(result.message);
}
}
else {
alert('success');
}
});
}
}
function errorHandler(error: CdvPurchase.IError) {
const errorElement = document.getElementById('error');
if (!errorElement) return;
errorElement.textContent = `ERROR ${error.code}: ${error.message}`;
setTimeout(() => {
errorElement.innerHTML = '<br/>';
}, 10000);
if (error.code === CdvPurchase.ErrorCode.LOAD_RECEIPTS) {
// Cannot load receipt, ask user to refresh purchases.
setTimeout(() => {
alert('Cannot access purchase information. Use "Refresh" to try again.');
}, 1);
}
}
function restorePurchases() {
log.info('restorePurchases()');
CdvPurchase.store.restorePurchases()
.then(() => {
alert('purchases restored');
})
.catch(err => {
alert('failed to restore purchases: ' + err);
});
}
function handlePaymentRequestStatus(promise: CdvPurchase.PaymentRequestPromise) {
promise
.cancelled(() => setStatusMessage(null))
.failed(error => { alert(error.message); setStatusMessage(''); })
.initiated(t => setStatusMessage('Processing payment...'))
.approved(t => setStatusMessage('Processing approved, verifying...'))
.finished(t => {
alert('payment successful');
setStatusMessage('Payment successful');
});
}
function launchTestPayment() {
setStatusMessage('Select payment method...');
const status = CdvPurchase.store.requestPayment({
platform: CdvPurchase.Platform.TEST,
items: [{
id: 'my-product-1',
title: 'My Product #1',
pricing: {
priceMicros: 1.49 * 1000000,
}
}, {
id: 'my-product-2',
title: 'My Product #2',
pricing: {
priceMicros: 2.99 * 1000000,
}
}],
currency: 'USD',
description: 'This this the description of the payment request',
});
handlePaymentRequestStatus(status);
}
function launchBraintreePayment() {
setStatusMessage('Select payment method...');
const address: CdvPurchase.PostalAddress = {
givenName: 'Jill',
surname: 'Doe',
phoneNumber: '5551234567',
streetAddress1: '555 Smith St',
streetAddress2: '#2',
countryCode: 'US',
locality: 'Chicago',
region: 'IL', // ISO-3166-2 code
postalCode: '12345',
};
const status = CdvPurchase.store.requestPayment({
platform: CdvPurchase.Platform.BRAINTREE,
items: [{
id: 'my-product-1',
title: 'My Product #1',
pricing: {
priceMicros: 1.49 * 1000000,
}
}, {
id: 'my-product-2',
title: 'My Product #2',
pricing: {
priceMicros: 2.99 * 1000000,
}
}],
currency: 'USD',
description: 'This is the description of the payment request',
billingAddress: address,
});
handlePaymentRequestStatus(status);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment