This tutorial provides a detailed walkthrough of implementing a complete subscription system using Iaptic and Stripe.
Overview
We'll build a subscription system with:
- Product listing (subscriptions and one-time purchases)
- Purchase and subscription flows
- Plan management and changes
- Detailed subscription information display
- Error handling and user feedback
Prerequisites
- Iaptic account with Stripe configured
- Basic understanding of HTML/JavaScript
- Text editor or IDE
Step 1: Project Setup
First, create your HTML structure:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Subscription System</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<!-- Message Container -->
<div id="message-container"></div>
<!-- Current Subscription -->
<div id="subscription-container"></div>
<!-- Product Lists -->
<div id="pricing-container"></div>
<div id="onetime-container"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/iaptic-js.js"></script>
<script src="app.js"></script>
</body>
</html>
Step 2: Initialize Iaptic
Create app.js and set up Iaptic:
const iaptic = IapticJS.createAdapter({
type: 'stripe',
appName: '[YOUR_APP_NAME]',
apiKey: '[YOUR_PUBLIC_API_KEY]',
stripePublicKey: '[YOUR_STRIPE_PUBLIC_KEY]'
});
// Message handling system
function showMessage(type, message) {
const container = document.getElementById('message-container');
const alertClass = {
success: 'alert-success',
error: 'alert-danger',
warning: 'alert-warning'
}[type] || 'alert-info';
container.innerHTML = `
<div class="alert ${alertClass} alert-dismissible fade show" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
container.scrollIntoView({ behavior: 'smooth' });
}
Step 3: Product Display System
Implement the product display logic:
async function displayProducts() {
const subscriptionContainer = document.getElementById('pricing-container');
const onetimeContainer = document.getElementById('onetime-container');
try {
const products = await iaptic.getProducts();
// Split products by type
const subscriptionProducts = products.filter(p =>
p.type === 'paid subscription' &&
p.metadata?.canPurchase !== 'false'
);
const otherProducts = products.filter(p =>
['non_consumable', 'consumable'].includes(p.type) &&
p.metadata?.canPurchase !== 'false'
);
// Render products
subscriptionContainer.innerHTML = `
<h2>Subscription Plans</h2>
<div class="row">
${renderSubscriptionProducts(subscriptionProducts)}
</div>
`;
onetimeContainer.innerHTML = `
<h2>One-time Purchases</h2>
<div class="row">
${renderOtherProducts(otherProducts)}
</div>
`;
} catch (error) {
showMessage('error', 'Failed to load products: ' + error.message);
}
}
function renderSubscriptionProducts(products) {
return products.map(product => `
<div class="col-md-4 mb-4">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">${product.title}</h5>
<p class="card-text">${product.description || ''}</p>
${renderOffers(product.offers, 'handleSubscription')}
</div>
</div>
</div>
`).join('');
}
function renderOtherProducts(products) {
return products.map(product => `
<div class="col-md-4 mb-4">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">${product.title}</h5>
<p class="card-text">${product.description || ''}</p>
${renderOffers(product.offers, 'handlePurchase')}
</div>
</div>
</div>
`).join('');
}
function renderOffers(offers, handlerName) {
return offers.map(offer => {
const phase = offer.pricingPhases[offer.pricingPhases.length - 1];
return `
<div class="text-center mb-3">
<div class="h4">
${IapticJS.Utils.formatCurrency(phase.priceMicros, phase.currency)}
${phase.billingPeriod ? `
<small class="text-muted">
/${IapticJS.Utils.formatBillingPeriodEN(phase.billingPeriod)}
</small>
` : ''}
</div>
<button class="btn btn-primary"
onclick="${handlerName}('${offer.id}')">
${phase.billingPeriod ? 'Subscribe' : 'Purchase'}
</button>
</div>
`;
}).join('');
}
Step 4: Purchase Handlers
Implement the purchase and subscription handlers:
async function handlePurchase(offerId) {
try {
await iaptic.initCheckoutSession({
offerId,
applicationUsername: 'user_dev', // Replace with actual user ID
successUrl: returnUrl('success'),
cancelUrl: returnUrl('cancel')
});
} catch (error) {
showMessage('error', 'Purchase failed: ' + error.message);
}
}
async function handleSubscription(offerId) {
try {
await iaptic.initCheckoutSession({
offerId,
applicationUsername: 'user_dev', // Replace with actual user ID
successUrl: returnUrl('success'),
cancelUrl: returnUrl('cancel')
});
} catch (error) {
showMessage('error', 'Subscription failed: ' + error.message);
}
}
function returnUrl(status) {
return `${window.location.href.split('#')[0]}#${status}`;
}
Step 5: Subscription Management
Add subscription display and management:
async function displaySubscriptionDetails() {
const container = document.getElementById('subscription-container');
if (!container) return;
try {
const purchases = await iaptic.getPurchases();
const products = await iaptic.getProducts();
if (!purchases || purchases.length === 0) {
container.innerHTML = '';
return;
}
const purchase = purchases[0];
const productId = purchase.productId.replace('stripe:', '');
const product = products.find(p => p.id === productId);
container.innerHTML = `
<div class="card mb-4">
<div class="card-header">
<h3 class="card-title">Current Subscription</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h4>${product?.title || 'Subscription'}</h4>
<p>${product?.description || ''}</p>
<p>
<strong>Status:</strong>
<span class="badge bg-${purchase.renewalIntent === 'Renew' ? 'success' : 'warning'}">
${purchase.renewalIntent === 'Renew' ? 'Active' : 'Canceling'}
</span>
</p>
<p>
<strong>Next billing:</strong>
${new Date(purchase.expirationDate).toLocaleDateString()}
</p>
</div>
<div class="col-md-6 text-end">
<button class="btn btn-primary"
onclick="handleManageSubscription()">
Manage Subscription
</button>
</div>
</div>
</div>
</div>
`;
} catch (error) {
showMessage('error', 'Failed to load subscription: ' + error.message);
}
}
async function handleManageSubscription() {
try {
await iaptic.redirectToCustomerPortal({
returnUrl: window.location.href
});
} catch (error) {
showMessage('error', 'Failed to open portal: ' + error.message);
}
}
Step 6: Initialize Everything
Set up page initialization and URL handling:
// Check URL hash for purchase/subscription status
async function checkUrlHash() {
const hash = window.location.hash.substring(1);
if (hash === 'success') {
showMessage('success', 'Payment successful! Thank you for your purchase.');
history.replaceState(null, '', window.location.pathname);
} else if (hash === 'cancel') {
showMessage('warning', 'Payment cancelled.');
history.replaceState(null, '', window.location.pathname);
}
}
// Initialize page
document.addEventListener('DOMContentLoaded', async () => {
await displaySubscriptionDetails();
await displayProducts();
checkUrlHash();
});
// Handle hash changes
window.addEventListener('hashchange', checkUrlHash);
Common Issues and Solutions
- Missing Containers: Ensure all HTML containers exist and have correct IDs
- CORS Issues: Verify your domain is whitelisted in Iaptic dashboard
- Stripe Key: Double-check your Stripe public key configuration
- Purchase Flow: Always handle both success and failure cases
- Plan Changes: Remember to refresh the UI after successful plan changes
Next Steps
- Add webhook handling for server-side events
- Implement user authentication
- Set up test mode for development
- Add analytics to track conversion
Complete Example
Here's a full working implementation:
<!-- file: index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Iaptic+Stripe Demo</title>
<link rel="stylesheet" href="node_modules/@tabler/core/dist/css/tabler.min.css">
<script src="node_modules/@tabler/core/dist/js/tabler.min.js" defer></script>
<style>
.pricing-container {
display: flex;
justify-content: center;
gap: 2rem;
padding: 2rem;
flex-wrap: wrap;
}
.price-card {
width: 300px;
}
#message-container {
max-width: 600px;
margin: 1rem auto;
padding: 0 1rem;
}
</style>
</head>
<body>
<div class="page">
<div class="container-xl">
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center justify-content-center">
<div class="col-auto">
<h2 class="page-title">
Iaptic+Stripe Demo
</h2>
</div>
</div>
</div>
</div>
<div id="message-container"></div>
<div id="subscription-container" class="mb-4"></div>
<div id="pricing-container" class="pricing-container"></div>
<div id="onetime-container" class="pricing-container"></div>
</div>
</div>
<!-- script src="lib/dist/iaptic-stripe.js"></script -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/iaptic-js.js"></script>
<script src="credentials.js"></script>
<script src="index.js"></script>
</body>
</html>
Next Steps
- Learn about webhook handling
- Implement user authentication
- Set up test mode
- Add analytics
- Explore access control