9 min read

Building a Subscription System

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

  1. Missing Containers: Ensure all HTML containers exist and have correct IDs
  2. CORS Issues: Verify your domain is whitelisted in Iaptic dashboard
  3. Stripe Key: Double-check your Stripe public key configuration
  4. Purchase Flow: Always handle both success and failure cases
  5. Plan Changes: Remember to refresh the UI after successful plan changes

Next Steps

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