9 min read

Stripe Integration Patterns

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]