-
-
Save robertopc/1f01bfc2a6fc625a4b358b4f7bb5935a to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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