6 min read

Implement Plan Changes

This guide explains how to handle subscription plan changes (upgrades/downgrades) in your application.

Customer Portal Method

The simplest way to handle plan changes is through Stripe's Customer Portal:

await iaptic.redirectToCustomerPortal({
  id: 'sub_xyz',
  accessKey: 'ak_123',
  returnUrl: 'https://example.com/account'
});

This provides a full-featured portal where customers can:

  • Change plans
  • Update payment methods
  • View invoices
  • Manage billing

Direct Plan Change

For more control, use the direct plan change endpoint:

const response = await iaptic.changePlan({
  id: 'sub_xyz',
  accessKey: 'ak_123',
  offerId: 'stripe:prod_xyz#price_xyz',
  successUrl: 'https://example.com/success',
  cancelUrl: 'https://example.com/cancel'
});

// Handle the updated subscription
console.log('New plan:', response.purchase);

Custom UI Implementation

Create your own plan selection interface:

async function showPlanSelector() {
  try {
    // 1. Fetch available plans
    const { products } = await iaptic.getProducts();
    
    // 2. Get current subscription
    const { purchases } = await iaptic.getPurchases();
    const currentPlan = purchases[0];
    
    // 3. Filter available upgrades/downgrades
    const availablePlans = products.filter(p => 
      p.type === 'paid subscription' &&
      p.metadata?.can_purchase === 'true' &&
      p.id !== currentPlan.productId
    );
    
    // 4. Show UI
    renderPlanOptions(availablePlans, currentPlan);
  } catch (error) {
    console.error('Failed to load plans:', error);
  }
}

async function changePlan(newOfferId) {
  try {
    const { purchases, new_access_keys } = await iaptic.changePlan({
      offerId: newOfferId
    });
    
    // Store new access key if provided
    if (new_access_keys) {
      for (const [subId, key] of Object.entries(new_access_keys)) {
        localStorage.setItem(`stripe_access_key_${subId}`, key);
      }
    }
    
    // Update UI with new plan
    updateSubscriptionDisplay(purchases[0]);
  } catch (error) {
    console.error('Plan change failed:', error);
  }
}

Handle Plan Change Events

Monitor plan changes through webhooks:

if (event.type === 'customer.subscription.updated') {
  const subscription = event.data.object;
  const previousAttributes = event.data.previous_attributes;
  
  if (previousAttributes.items) {
    const oldPriceId = previousAttributes.items.data[0].price.id;
    const newPriceId = subscription.items.data[0].price.id;
    
    // Handle plan change
    onPlanChanged(subscription.id, oldPriceId, newPriceId);
  }
}

Best Practices

  1. Proration Handling

    • Clearly show proration amounts
    • Explain billing changes
    • Handle credit application
  2. UI/UX

    • Show plan comparison
    • Highlight differences
    • Confirm changes
    • Show effective date
  3. Error Handling

    • Payment failures
    • Invalid plans
    • Network issues
    • Access key rotation
  4. Feature Management

    • Update feature access
    • Handle downgrades
    • Manage quotas

Common Issues

Plan Change Failed

  1. Check subscription status
  2. Verify price is active
  3. Validate access key
  4. Check payment method

Features Not Updated

  1. Clear feature cache
  2. Refresh subscription status
  3. Check webhook delivery
  4. Verify metadata

Billing Issues

  1. Check proration settings
  2. Verify price configuration
  3. Check customer balance
  4. Review invoice items

Example: Complete Plan Change Flow

class PlanManager {
  constructor(iaptic) {
    this.iaptic = iaptic;
  }

  async showPlanSelector() {
    const { products } = await this.iaptic.getProducts();
    const { purchases } = await this.iaptic.getPurchases();
    
    return {
      currentPlan: purchases[0],
      availablePlans: products.filter(p => 
        p.type === 'paid subscription' &&
        p.metadata?.can_purchase === 'true'
      ),
      pricing: products.map(p => ({
        id: p.id,
        price: this.iaptic.formatCurrency(
          p.offers[0].pricingPhases[0].priceMicros,
          p.offers[0].pricingPhases[0].currency
        ),
        interval: this.iaptic.formatBillingPeriodEN(
          p.offers[0].pricingPhases[0].billingPeriod
        )
      }))
    };
  }

  async changePlan(newOfferId) {
    try {
      // 1. Change plan
      const { purchase, new_access_keys } = await this.iaptic.changePlan({
        offerId: newOfferId
      });

      // 2. Update access keys
      if (new_access_keys) {
        for (const [subId, key] of Object.entries(new_access_keys)) {
          localStorage.setItem(`stripe_access_key_${subId}`, key);
        }
      }

      // 3. Update features
      await this.updateFeatures(purchase);

      return purchase;
    } catch (error) {
      console.error('Plan change failed:', error);
      throw error;
    }
  }

  async updateFeatures(purchase) {
    // Implement your feature update logic
  }
}