This guide will walk you through integrating in-app purchases into your application, explaining each step and concept along the way.
Platform Selection
First, select your client platform to get started:
Overview
We'll build the integration in these steps:
- Install and configure the cordova plugin
- Set up state management
- Initialize the store
- Handle purchase events
- Build the UI
- Test the integration
Step 1: Plugin Setup
First, install the Cordova Purchase Plugin:
cordova plugin add cordova-plugin-purchase
Then create your configuration:
window.IAPTIC_CONFIG = {
appName: '[APP_NAME]',
apiKey: '[PUBLIC_KEY]'
}
// Define your product IDs (for Apple, Google or both)
const APPLE_SUBSCRIPTIONS = ['monthly_subscription'];
const GOOGLE_SUBSCRIPTIONS = ['monthly', 'yearly_2024'];
Step 2: State Management
Let's create a simple state management system to track:
- Store readiness
- Processing state
- Active subscriptions
- Available products
- Errors
class State {
ready: boolean = false;
error: string = '';
isProcessingOrder: boolean = false;
activeSubscription?: CdvPurchase.VerifiedPurchase;
products: CdvPurchase.Product[] = [];
render: (state: State) => void;
constructor(render: (state: State) => void) {
this.render = render;
}
set(attr: Partial<State>) {
Object.assign(this, attr);
this.render(this); // re-render the view when the state changes
}
}
We use this state to render the UI when something changes (the SDK you're using probably provides a better way to do this, this is just a platform-agnostic example).
Step 3: Store Initialization
Now we'll create a service to handle store initialization and purchases:
class SubscriptionService {
constructor(state: State) {
this.state = state;
}
async initialize(): Promise<void> {
// Register your products with the store
this.registerProducts();
// Handle purchase events
this.setupEventHandlers();
// Set up receipt validation with Iaptic
const iaptic = new CdvPurchase.Iaptic(IAPTIC_CONFIG);
CdvPurchase.store.validator = iaptic.validator;
// Initialize the store with platform-specific options
await this.initializeStore();
this.state.set({ ready: true });
}
private registerProducts() {
CdvPurchase.store.register([
...APPLE_SUBSCRIPTIONS.map(id => ({
id,
platform: CdvPurchase.Platform.APPLE_APPSTORE,
type: CdvPurchase.ProductType.PAID_SUBSCRIPTION
})),
...GOOGLE_SUBSCRIPTIONS.map(id => ({
id,
platform: CdvPurchase.Platform.GOOGLE_PLAY,
type: CdvPurchase.ProductType.PAID_SUBSCRIPTION
}))
]);
}
private async initializeStore() {
await CdvPurchase.store.initialize([
CdvPurchase.Platform.GOOGLE_PLAY,
{
platform: CdvPurchase.Platform.APPLE_APPSTORE,
options: { needAppReceipt: true }
}
]);
}
}
Let's initialize the service at startup, in the HTML file for example:
const state = new State(new View().render);
window.subscriptionService = new SubscriptionService(state);
window.subscriptionService.initialize();
Step 5: Building the Products UI
Let's build our UI in two stages: first displaying products, then handling purchases.
For this example, we'll use a simple javascript HTML renderer (to keep this example platform-agnostic). Your SDK will probably provide a better way to do this.
/* file: html.ts */
export type HTMLContent = ((string | undefined | null)[]) | string | undefined | null;
export type HTMLAttributes = {
className?: string;
onclick?: VoidFunction | ((ev: Event) => void) | string;
src?: string;
style?: string;
type?: string;
id?: string;
value?: string;
}
const ATTRIBUTE_NAMES: { [key: string]: string } = {
className: "class",
}
/**
* Unsafe mini html library
*/
export class HTML {
static tag(tag: string, content: HTMLContent, attributes?: HTMLAttributes) {
if (content === null) return '';
const attrString = attributes
? Object.keys(attributes)
.map(key => `${ATTRIBUTE_NAMES[key] || key}="${cleanAttribute((attributes as any)[key] ?? '')}"`)
.join(' ')
: '';
return `<${tag}${attrString.length > 0 ? ' ' + attrString : ''}>${HTML.toString(content)}</${tag}>`;
}
static pre(lines: HTMLContent) { return HTML.tag('pre', lines); }
static div(lines: HTMLContent, attributes?: HTMLAttributes) { return HTML.tag('div', lines, attributes); }
static span(lines: HTMLContent, attributes?: HTMLAttributes) { return HTML.tag('span', lines, attributes); }
static p(lines: HTMLContent, attributes?: HTMLAttributes) { return HTML.tag('p', lines, attributes); }
static b(lines: HTMLContent, attributes?: HTMLAttributes) { return HTML.tag('b', lines, attributes); }
static h1(lines: HTMLContent, attributes?: HTMLAttributes) { return HTML.tag('h1', lines, attributes); }
static h2(lines: HTMLContent, attributes?: HTMLAttributes) { return HTML.tag('h2', lines, attributes); }
static center(lines: HTMLContent, attributes?: HTMLAttributes) { return HTML.tag('center', lines, attributes); }
static button(lines: HTMLContent, attributes?: HTMLAttributes) { return HTML.tag('button', lines, attributes); }
static img(lines: HTMLContent, attributes?: HTMLAttributes) { return HTML.tag('img', lines, attributes); }
static input(lines: HTMLContent, attributes?: HTMLAttributes) { return HTML.tag('input', lines, attributes); }
static label(lines: HTMLContent, attributes?: HTMLAttributes) { return HTML.tag('label', lines, attributes); }
static form(lines: HTMLContent, attributes?: HTMLAttributes) { return HTML.tag('form', lines, attributes); }
static toString(lines: HTMLContent) {
if (lines === null || lines === undefined) return '';
if (typeof lines === 'string') return lines;
return lines.filter(l => l !== null).join('\n');
}
}
function cleanAttribute(s: string | VoidFunction): string {
if (typeof s === 'string') {
return s.replace(/"/g, `'`);
}
else { // Function
return cleanAttribute('(' + s + ')()');
}
}
And implement our page renderer render
method:
First, let's create a basic UI that shows available products and allows placing orders:
class PageRenderer {
render(state: State) {
return HTML.div([
HTML.h2('Available Subscriptions'),
this.renderProducts(state.products)
]);
}
private renderProducts(products: CdvPurchase.Product[]) {
if (!products.length) {
return HTML.div(
'Loading products...',
{ className: "w3-container" }
);
}
return HTML.div(
products.map(product => this.renderProduct(product))
);
}
private renderProduct(product: CdvPurchase.Product) {
return HTML.div([
HTML.h3(product.title),
HTML.p(product.description),
this.renderOffers(product)
], { className: "w3-card w3-padding w3-margin" });
}
private renderOffers(product: CdvPurchase.Product) {
if (!product.offers) return '';
return HTML.div(
product.offers.map(offer =>
HTML.div([
HTML.div(`${offer.pricingPhases[0].price}`),
HTML.button(
'Subscribe',
{
onclick: `window.subscriptionService.order("${offer.id}")`,
className: "w3-button w3-blue"
}
)
], { className: "w3-section" })
)
);
}
}
At this stage, our UI:
- Shows a list of available products
- Displays product details (title, description)
- Shows pricing for each offer
- Provides purchase buttons
- Handles basic loading state
Let's implement the order
method in the SubscriptionService
class:
class SubscriptionService {
// ...
private order(offerId: string) {
CdvPurchase.store.order(offerId).then(transaction => {
this.state.set({ isProcessingOrder: false });
}).catch(error => {
this.state.set({ error: error.message });
});
}
}
Step 4: Purchase Event Handling
The purchase flow involves several steps:
- User initiates purchase
- Store processes payment
- App verifies receipt
- App acknowledges purchase
Here's how to handle these events:
class SubscriptionService {
// ...
private setupEventHandlers() {
CdvPurchase.store.when()
// Products loaded from store
.productUpdated(() => {
this.state.set({ products: CdvPurchase.store.products });
})
// Purchase approved by store
.approved(transaction => {
this.state.set({ isVerifying: true });
transaction.verify();
})
// Receipt verified by Iaptic
.verified(receipt => {
this.state.set({
purchases: CdvPurchase.store.verifiedPurchases,
isVerifying: false
});
receipt.finish();
})
// Handle verification failures
.unverified(receipt => {
this.state.set({
isVerifying: false,
error: 'Purchase verification failed'
});
receipt.finish();
});
// Handle purchase completion
.finished(() => {
this.state.set({ isProcessingOrder: false });
});
}
}
Stage 2: Purchase Handling
Now let's enhance our UI to handle purchases and show their status:
class PageRenderer {
render(state: State) {
return HTML.div([
// Purchase status section
this.renderPurchaseStatus(state),
// Products section (from Stage 1)
HTML.h2('Available Subscriptions'),
this.renderProducts(state)
]);
}
private renderPurchaseStatus(state: State) {
// Show processing state
if (state.isProcessingOrder) {
return HTML.div([
HTML.img('', { src: "img/loading.gif" }),
HTML.p('Processing your purchase...')
], { className: "w3-container w3-center" });
}
// Show verification state
if (state.isVerifying) {
return HTML.div([
HTML.img('', { src: "img/loading.gif" }),
HTML.p('Verifying your purchase...')
], { className: "w3-container w3-center" });
}
// Show error if any
if (state.error) {
return HTML.div(
`Error: ${state.error}`,
{ className: "w3-panel w3-red" }
);
}
// Show active subscription
if (state.activeSubscription) {
return HTML.div([
HTML.h3('Active Subscription'),
HTML.p(`Valid until: ${state.activeSubscription.expiryDate}`),
HTML.p('✅ Premium features unlocked!')
], { className: "w3-panel w3-green" });
}
return HTML.div(
'No active subscription',
{ className: "w3-panel w3-grey" }
);
}
private renderProducts(state: State) {
// Disable purchase buttons while processing
const isDisabled = state.isProcessingOrder || state.isVerifying;
if (!state.products.length) {
return HTML.div(
'Loading products...',
{ className: "w3-container" }
);
}
return HTML.div(
state.products.map(product =>
HTML.div([
HTML.h3(product.title),
HTML.p(product.description),
this.renderOffers(product, isDisabled)
], { className: "w3-card w3-padding w3-margin" })
)
);
}
private renderOffers(product: CdvPurchase.Product, isDisabled: boolean) {
if (!product.offers) return '';
return HTML.div(
product.offers.map(offer =>
HTML.div([
HTML.div(`${offer.pricingPhases[0].price}`),
HTML.button(
'Subscribe',
{
onclick: () => window.store.order(offer),
className: "w3-button w3-blue",
disabled: isDisabled || !offer.canPurchase
}
)
], { className: "w3-section" })
)
);
}
}
In Stage 2, we've added:
- Purchase status display showing:
- Processing state
- Verification state
- Error messages
- Active subscription details
- Button state management:
- Disabled during processing
- Disabled during verification
- Disabled when purchase not available
The UI now provides complete feedback about the purchase process while maintaining a clean and organized structure.
Step 6: Testing
Launch the app on a device or simulator and test various scenarios:
- Successful purchases
- Failed purchases
- Receipt validation
- Subscription management
Common Issues and Solutions
- Security Policy Errors: Add validator.iaptic.com to your Content-Security-Policy
- Missing Purchases: Initialize the plugin at app startup
- Unacknowledged Purchases: Implement proper error handling
- Receipt Validation Failures: Check network connectivity and API keys
Next Steps
- Learn about error handling
- Implement receipt validation
- Set up subscription lifecycle management
Web/Stripe Setup
Coming soon! This section will contain instructions for setting up web-based payments using Stripe.
React Native Setup
Coming soon! This section will contain instructions for setting up React Native.
Next Steps
- Learn about error handling
- Implement receipt validation
- Set up subscription lifecycle management