This document describes common integration patterns and best practices for implementing Stripe with iaptic.
Basic Subscription Flow
sequenceDiagram
Client->>Iaptic: 1. Get products
Iaptic->>Client: Available products
Client->>Iaptic: 2. Create checkout
Iaptic->>Client: Session + access key
Client->>Stripe: 3. Complete payment
Stripe->>Iaptic: 4. Webhook event
Client->>Iaptic: 5. Verify status
Implementation
class SubscriptionManager {
constructor(iaptic) {
this.iaptic = iaptic;
}
async startSubscription(userId) {
// 1. Get available products
const { products } = await this.iaptic.getProducts();
// 2. Create checkout session
const response = await this.iaptic.createStripeCheckout({
offerId: products[0].offers[0].id,
applicationUsername: userId,
successUrl: window.location.origin + '/success',
cancelUrl: window.location.origin + '/cancel'
});
// 3. Store access key
localStorage.setItem('stripe_access_key', response.accessKey);
// 4. Redirect to checkout
window.location.href = response.url;
}
async verifySubscription() {
const accessKey = localStorage.getItem('stripe_access_key');
if (!accessKey) return null;
const { purchases } = await this.iaptic.getPurchases(
undefined,
accessKey
);
return purchases[0];
}
}
Feature Management
Feature Flags in Metadata
// Product configuration in Stripe
{
metadata: {
features: 'api,backup,support',
limits: JSON.stringify({
storage: '50GB',
users: 5,
projects: 10
})
}
}
// Client-side usage
class FeatureManager {
constructor(purchase) {
this.features = purchase.metadata?.features?.split(',') ?? [];
this.limits = JSON.parse(purchase.metadata?.limits ?? '{}');
}
hasFeature(feature) {
return this.features.includes(feature);
}
getLimit(key) {
return this.limits[key];
}
}
Plan Changes
Upgrade Flow
async function upgradePlan(currentPlan, newPlan) {
try {
// 1. Verify current subscription
const { purchases } = await iaptic.getPurchases();
if (!purchases.length) throw new Error('No active subscription');
// 2. Change plan
await iaptic.changePlan({
id: purchases[0].purchaseId,
offerId: newPlan.offers[0].id
});
// 3. Update features
await refreshFeatures();
} catch (error) {
handleError(error);
}
}
User Management
Single User Pattern
class UserSubscription {
constructor(iaptic, userId) {
this.iaptic = iaptic;
this.userId = userId;
}
async getStatus() {
const { purchases } = await this.iaptic.getPurchases();
return purchases.find(p =>
p.applicationUsername === this.userId
);
}
async manageSubscription() {
await this.iaptic.redirectToCustomerPortal({
returnUrl: window.location.origin + '/account'
});
}
}
Team Pattern
class TeamSubscription {
constructor(iaptic, teamId) {
this.iaptic = iaptic;
this.teamId = teamId;
}
async addMember(userId) {
const { purchases } = await this.iaptic.getPurchases();
const subscription = purchases[0];
const users = subscription.metadata.application_username
.split(',')
.concat(`team/${userId}`);
await stripe.subscriptions.update(subscription.id, {
metadata: {
application_username: users.join(','),
team_id: this.teamId
}
});
}
}
Error Recovery
Access Key Recovery
class AccessKeyManager {
static async recover(subscriptionId) {
try {
// Try stored key first
const storedKey = localStorage.getItem(
`stripe_key_${subscriptionId}`
);
// Verify it works
const { purchases } = await iaptic.getPurchases(
subscriptionId,
storedKey
);
// Handle rotation if needed
if (purchases.new_access_keys) {
this.storeNewKeys(purchases.new_access_keys);
}
return true;
} catch (error) {
// Clear invalid key
localStorage.removeItem(
`stripe_key_${subscriptionId}`
);
return false;
}
}
}
Webhook Processing
Event Handler
class WebhookProcessor {
async handleEvent(event) {
switch (event.type) {
case 'customer.subscription.updated':
await this.handleSubscriptionUpdate(event);
break;
case 'customer.subscription.deleted':
await this.handleSubscriptionDeleted(event);
break;
// ... handle other events
}
}
async handleSubscriptionUpdate(event) {
const subscription = event.data.object;
const previousAttributes = event.data.previous_attributes;
// Check for plan change
if (previousAttributes.items) {
await this.handlePlanChange(subscription);
}
// Check for ownership change
if (previousAttributes.metadata?.application_username) {
await this.handleOwnershipChange(subscription);
}
}
}
State Management
Client-side Cache
class SubscriptionCache {
constructor(ttl = 30000) { // 30 seconds
this.cache = new Map();
this.ttl = ttl;
}
async getSubscription(id, accessKey) {
const cached = this.cache.get(id);
if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.data;
}
const fresh = await iaptic.getPurchases(id, accessKey);
this.cache.set(id, {
data: fresh,
timestamp: Date.now()
});
return fresh;
}
}
Testing Patterns
Subscription Tests
describe('Subscription Management', () => {
it('handles plan changes', async () => {
// 1. Create test subscription
const sub = await createTestSubscription();
// 2. Change plan
await iaptic.changePlan({
id: sub.id,
offerId: 'stripe:prod_test#price_new'
});
// 3. Verify change
const { purchases } = await iaptic.getPurchases(
sub.id,
sub.accessKey
);
expect(purchases[0].productId).toBe('stripe:prod_test');
});
});
Subscription Lifecycle
stateDiagram-v2
[*] --> Checkout
Checkout --> Active: Payment Success
Checkout --> Abandoned: Cancel
Active --> Updated: Change Plan
Active --> Cancelled: Cancel
Active --> PastDue: Payment Failed
PastDue --> Active: Payment Success
PastDue --> Cancelled: Grace Period End
Cancelled --> [*]
Webhook Processing
flowchart TD
Event[Webhook Event] --> Validate{Validate}
Validate -->|Valid| Process[Process Event]
Validate -->|Invalid| Reject[Reject Event]
Process --> Update[Update Status]
Process --> Notify[Notify Client]
Process --> Log[Log Event]