# Frontend 3D Secure Implementation Guide ## ๐ Visual Flow Overview ```mermaid flowchart TD Start([User Clicks Subscribe]) --> Immediate{ImmediateCharge?} Immediate -->|YES| Direct[Direct Subscription Pattern] Immediate -->|NO| Setup[Setup Intent Pattern] Direct --> CreateSub[POST /api/subscription/update] CreateSub --> Check3DS{Requires3DS?} Check3DS -->|YES| Show3DS[Show 3DS Popup] Check3DS -->|NO| Success1[Subscription Active โ] Show3DS --> Customer3DS[Customer Authenticates] Customer3DS --> Auto[Stripe Auto-Charges] Auto --> Webhook[Webhook Updates Status] Webhook --> Success2[Subscription Active โ] Setup --> SetupCard[POST /api/subscription/setupCard] SetupCard --> CheckSetup{Requires3DS?} CheckSetup -->|YES| Setup3DS[Show 3DS Popup] CheckSetup -->|NO| CardAuth[Card Authenticated] Setup3DS --> SetupComplete[Customer Authenticates] SetupComplete --> CardAuth CardAuth --> CreateNoCharge[Create SubscriptionsNo Immediate Charge] CreateNoCharge --> Success3[Subscriptions Active โ] style Direct fill:#e1f5e1 style Setup fill:#fff3cd style Success1 fill:#d4edda style Success2 fill:#d4edda style Success3 fill:#d4edda style Show3DS fill:#f8d7da style Setup3DS fill:#f8d7da ``` ## Problem Statement When creating subscriptions with 3D Secure (3DS) cards: - Payment intent returns `requires_action` status requiring customer authentication - Creating multiple subscriptions (package + addons) with same card triggers 3DS twice - Results in partial subscription creation if first 3DS not completed before second charge - Complex error handling and poor user experience ## Solutions ### โญ Solution 1: Direct Subscription Pattern (Recommended for Immediate Charges) **UPDATED RECOMMENDATION**: For subscriptions with immediate charges, handle 3DS during subscription creation. **Benefits**: - โ **Single authentication per subscription** - โ No double 3DS (Setup Intent + Payment) - โ Works for single OR multiple subscriptions - โ Simpler flow **When to Use**: - โ Creating subscription with immediate charge (no trial) - โ Upgrading/downgrading existing subscription - โ Package + addon creation with immediate billing - โ Any scenario where first payment happens NOW **โ ๏ธ CRITICAL: Multiple Subscriptions Require Multiple 3DS** **Test Date**: January 16, 2026 - **Confirmed Behavior**: ```javascript // Calling /update twice with package + addon: // 1st call: Package subscription โ requires 3DS โ // 2nd call: Addon subscription โ requires 3DS AGAIN โ // Result: Frontend must handle 3DS for EACH subscription ``` **Why?** - Each subscription creates its own PaymentIntent - Stripe does NOT reuse 3DS authentication between PaymentIntents - Even using the same payment method seconds apart requires separate authentication - Security policy - each payment attempt is independent **Implementation Impact**: - Frontend MUST be ready to handle 3DS popup for EACH `/update` call - Cannot batch authenticate multiple subscriptions - User sees 3DS prompt multiple times (expected Stripe behavior) **๐ก In Practice (Recommended Approach)**: - **This scenario is RARE** - most cards don't require 3DS - **When it happens**: Users understand multiple popups for multiple subscriptions - **Accept multiple popups** - simpler implementation, immediate charging - **No workarounds needed** - trial period adds unnecessary complexity **See**: [Direct Subscription Pattern](#direct-subscription-pattern) section below --- ### Solution 2: Setup Intent Pattern (For Future Charges Only) Pre-authenticate the card for **future off-session payments** only. **Benefits**: - โ Validates card without charging - โ Good for trial periods - โ For Reactivating/renewing subscriptions (cancel_at_period_end=false) work without immediate charge **When to Use**: - โ User updates cancel_at_period_end=false with NEW unverified card - โ Adding payment method during trial (no immediate charge) - โ Changing (default) payment method on active subscription (next charge is future) - โ Pre-validating card for scheduled billing **โ ๏ธ DO NOT Use For**: - โ Creating subscription with immediate charge - โ Upgrading/downgrading (causes double authentication) **Why Avoid for Immediate Charges?** - Setup Intent authenticates for **future off-session payments** - First payment is **on-session** โ requires 3DS AGAIN - Results in double authentication (bad UX) **See**: [Setup Intent Pattern](#setup-intent-pattern) section below --- ## ๐ Special Case: Reactivating Subscription with New Card ### Scenario User has subscription with `cancel_at_period_end=true` and wants to: 1. Update to `cancel_at_period_end=false` (reactivate) 2. Use a NEW payment method that requires 3DS ### Problem - Subscription is already active (no immediate charge) - Just changing `cancel_at_period_end` doesn't trigger payment - But new card needs authentication for FUTURE recurring charges ### Solution: Use Setup Intent **Step 1**: Authenticate new card with Setup Intent ```typescript // Frontend calls setupCard endpoint const result = await stripeService.setupCardAuthentication(custId, newPmId); if (result.requiresAction) { // Handle 3DS const { error } = await stripe.confirmCardSetup(result.clientSecret); if (error) { // Handle error return; } } ``` **Step 2**: Update subscription settings ```typescript // Now update cancel_at_period_end with authenticated card await subscriptionService.updateSettings({ cancel_at_period_end: false, default_payment_method: newPmId // Already authenticated via Setup Intent }); ``` **Why This Works**: - โ No immediate charge (subscription already active) - โ Card authenticated for future recurring payments - โ Next billing cycle uses authenticated payment method - โ No double authentication --- ## ๐ Decision Flow Chart ```mermaid flowchart TD Q1{Is there animmediate charge?} Q1 -->|YES| UseCase1["Use Direct Subscription Patternโข New subscriptionโข Upgrade/Downgradeโข Package + Addons"] Q1 -->|NO| UseCase2["Use Setup Intent Patternโข Trial periodโข Reactivation (cancel_at_period_end=false)โข Future billing"] UseCase1 --> Direct["Direct Pattern Flow:1. POST /update2. Handle 3DS if required3. Stripe auto-charges after 3DS"] UseCase2 --> Setup["Setup Intent Flow:1. POST /setupCard2. Handle 3DS if required3. POST /update (no charge)4. Subscriptions active"] Direct --> Result1["โ Immediate chargeโ May require 3DS popupโ Simple implementation"] Setup --> Result2["โ No immediate chargeโ Pre-authenticated cardโ Avoid double 3DS"] style UseCase1 fill:#e1f5e1 style UseCase2 fill:#fff3cd style Result1 fill:#d4edda style Result2 fill:#d4edda ``` --- ## ๐ฏ Setup Intent Pattern ### ๐ Flow Diagram: Setup Intent for Future Charges ```mermaid sequenceDiagram participant User participant Frontend participant Backend participant Stripe participant Bank Note over User,Bank: Phase 1: Authenticate Card (No Charge) User->>Frontend: Reactivate Subscription(cancel_at_period_end=false) Frontend->>Backend: POST /api/subscription/setupCard{custId, pmId} Backend->>Stripe: Create SetupIntent alt Card Requires 3DS Stripe-->>Backend: {requiresAction: true,client_secret} Backend-->>Frontend: {requiresAction: true,client_secret} Frontend->>User: Show 3DS Popup Frontend->>Stripe: confirmCardSetup(client_secret) Stripe->>Bank: Request Authentication Bank->>User: Show 3DS Challenge User->>Bank: Complete Authentication Bank-->>Stripe: Authentication Success Stripe-->>Frontend: SetupIntent: succeeded else No 3DS Required Stripe-->>Backend: {requiresAction: false} Backend-->>Frontend: {requiresAction: false} end Note over User,Bank: Phase 2: Update Subscription (No Charge) Frontend->>Backend: POST /api/subscription/updateSettings{cancel_at_period_end: false} Backend->>Stripe: Update Subscription Stripe-->>Backend: Subscription updated Backend-->>Frontend: Success Frontend->>User: Show Success โ Note over User,Bank: Phase 3: Future Billing (Off-Session) Stripe->>Stripe: Billing cycle ends Stripe->>Stripe: Auto-charge (no popup)using pre-authenticated card Stripe->>Backend: Webhook: invoice.payment_succeeded Backend->>User: Send Receipt Email ``` ### Overview This pattern separates card authentication from subscription creation: 1. **Step 1**: Authenticate card with `/api/subscription/setupCard` 2. **Step 2**: Handle 3DS if required (single popup) 3. **Step 3**: Create all subscriptions with authenticated card ### Backend Implementation **New Endpoint**: `POST /api/subscription/setupCard` **Code**: [controllers/subscription.js](../controllers/subscription.js#L769) **Request**: ```json { "custId": "cus_xxx", "pmId": "pm_xxx" } ``` **Response (No 3DS)**: ```json { "requiresAction": false, "status": "succeeded", "setupIntentId": "seti_xxx", "message": "Card authenticated successfully" } ``` **Response (3DS Required)**: ```json { "requiresAction": true, "clientSecret": "seti_xxx_secret_xxx", "setupIntentId": "seti_xxx", "status": "requires_action", "message": "Card authentication required" } ``` ### Frontend Implementation - Complete Example #### 1. Update Stripe Service **File**: `client/app/services/stripe.service.ts` ```typescript import { Injectable } from '@angular/core'; declare var Stripe: any; @Injectable() export class StripeService { private stripe: any; constructor() { // Initialize with your publishable key this.stripe = Stripe('pk_test_51LlCfSJxyI1MWs2Ty9utAc7QHhAa4YT6VPosvDdFtRaRQJchCLgd4NGvnarZQsCKiQUfJeOmnzs81w0AktP0N1o300Jd4q4m8n'); } /** * Authenticate card using SetupIntent (for pre-authentication) * @param clientSecret SetupIntent client_secret from backend * @returns Promise that resolves when authentication completes */ async confirmCardSetup(clientSecret: string): Promise { try { const result = await this.stripe.confirmCardSetup(clientSecret); if (result.error) { throw new Error(result.error.message); } return result.setupIntent; } catch (error) { console.error('Card setup authentication error:', error); throw error; } } /** * Handle 3DS authentication for payment (legacy - for single subscriptions) * @param clientSecret Payment Intent client_secret from backend * @returns Promise with payment intent result */ async handleCardAction(clientSecret: string): Promise { try { const result = await this.stripe.handleCardAction(clientSecret); if (result.error) { throw new Error(result.error.message); } return result.paymentIntent; } catch (error) { console.error('3DS authentication error:', error); throw error; } } } ``` --- ## ๐ Multi-Subscription 3DS Handling ### ๐ Flow Diagram: Package + Addon with 3DS ```mermaid sequenceDiagram participant User participant Frontend participant Backend participant Stripe Note over User,Stripe: First Subscription: Package User->>Frontend: Select Package + Addon Frontend->>Backend: POST /update{package: ess_1, addons: []} Backend->>Stripe: Create Package Subscription Stripe-->>Backend: Subscription: incompleterequires_action, client_secret Backend-->>Frontend: {requiresAction: true, client_secret} Frontend->>User: 3DS Popup #1 (Package) User->>Frontend: Complete Authentication Frontend->>Stripe: confirmCardPayment(client_secret) Stripe-->>Frontend: PaymentIntent: succeeded Note over Frontend,Stripe: โ ๏ธ Subscription still 'incomplete'! Frontend->>Frontend: Poll subscription status Stripe->>Stripe: Auto-charge card (background) Stripe->>Stripe: Update subscription โ 'active' Stripe->>Backend: Webhook: subscription.updated Backend->>Backend: Update DB Frontend->>Backend: GET /subscription/status Backend-->>Frontend: Subscription: active Frontend->>User: โ Package activated Note over User,Stripe: Second Subscription: Addon (Same Card!) Frontend->>Backend: POST /update{package: ess_1, addons: [addon_1]} Backend->>Stripe: Create Addon Subscription rect rgb(255, 200, 200) Note over Stripe: Each PaymentIntent = New 3DS Required! Stripe-->>Backend: Subscription: incompleterequires_action, client_secret Backend-->>Frontend: {requiresAction: true, client_secret} end Frontend->>User: 3DS Popup #2 (Addon) User->>Frontend: Complete Authentication AGAIN Frontend->>Stripe: confirmCardPayment(client_secret) Stripe-->>Frontend: PaymentIntent: succeeded Note over Frontend,Stripe: โ ๏ธ Subscription still 'incomplete'! Frontend->>Frontend: Poll subscription status Stripe->>Stripe: Auto-charge card (background) Stripe->>Stripe: Update subscription โ 'active' Stripe->>Backend: Webhook: subscription.updated Backend->>Backend: Update DB Frontend->>Backend: GET /subscription/status Backend-->>Frontend: Subscription: active Frontend->>User: โ Addon activated Note over User,Stripe: Result: 2 subscriptions = 2 popups + 2 polling cycles โ ๏ธ ``` ### ๐ Frequency Analysis ```mermaid pie title "Multi-Subscription 3DS Frequency" "No 3DS (most cards)" : 85 "Single subscription with 3DS" : 13 "Multi-subscription with 3DS" : 2 ``` **Recommendation**: Accept multiple popups - this is a <2% edge case. ### How Rare Is This? **In Production**: - 3DS required: ~5-15% of cards (mainly European/UK cards) - Multiple subscriptions: ~10-20% of checkouts - **Both together: <2% of checkouts** **Recommendation**: Accept multiple popups for this rare edge case. Don't over-engineer. --- ### Critical Behavior (Confirmed January 16, 2026) **Each subscription requires its own 3DS authentication**, even when: - Using the same payment method - Created seconds apart - Within the same user session ### Example Flow: Package + Addon Creation ```typescript // Step 1: Create package subscription const packageResult = await this.subscriptionService.updateSubscription({ package: 'ess_1', addons: [] // Will add addon after }); if (packageResult.requiresAction) { // Handle 3DS for package await this.stripeService.handleCardAction(packageResult.client_secret); console.log('โ Package 3DS completed'); } // Step 2: Create addon subscription (SAME card, seconds later) const addonResult = await this.subscriptionService.updateSubscription({ package: 'ess_1', // Already exists, no change addons: [{ price: 'addon_1', quantity: 1 }] }); if (addonResult.requiresAction) { // โ ๏ธ WILL REQUIRE 3DS AGAIN (not reused from package) await this.stripeService.handleCardAction(addonResult.client_secret); console.log('โ Addon 3DS completed'); } ``` ### Why Multiple 3DS Prompts? **Stripe Security Design**: - Each PaymentIntent is an independent payment attempt - 3DS authentication is tied to specific PaymentIntent (not payment method) - Cannot "carry over" authentication between PaymentIntents - Prevents replay attacks and ensures each charge is authorized ### User Experience Considerations **What User Sees**: 1. Selects package + addon 2. Clicks "Subscribe" 3. **First 3DS popup** - authenticates package subscription 4. Package created successfully 5. **Second 3DS popup** - authenticates addon subscription 6. Both subscriptions active **Best Practices**: - โ Show loading indicator between 3DS prompts - โ Display message: "Authenticating package..." then "Authenticating addon..." - โ Don't let user close dialog during authentication sequence - โ Handle cancellation gracefully (partial subscription state) - โ Don't promise "one authentication" for multiple subscriptions ### Alternative: Delay First Charge with Trial Period **โ ๏ธ EDGE CASE ONLY** - Not recommended for normal checkout flow! **When to Consider**: - โ Creating 5+ subscriptions at once (very rare) - โ Specific customer requirement to avoid multiple popups - โ **DO NOT use by default** - adds complexity for rare scenario **What `trial_period_days` Does**: - Subscription starts immediately (customer gets access NOW) - First charge is DELAYED by X days - No immediate PaymentIntent = no 3DS popup during creation - After trial ends, Stripe automatically charges (off-session, using pre-authenticated card) **Example: Avoid Multiple 3DS by Delaying Charge**: ```typescript // Step 1: Pre-authenticate card (ONE 3DS popup) const setupResult = await this.subscriptionService.setupCard(customerId, pmId); if (setupResult.requiresAction) { await this.stripeService.confirmCardSetup(setupResult.clientSecret); // โ Customer completed 3DS authentication } // Step 2: Create both subscriptions with 1-day trial (NO additional 3DS) const result = await this.subscriptionService.updateSubscription({ package: 'ess_1', addons: [{ price: 'addon_1', quantity: 1 }], trial_period_days: 1 // Delay charge by 1 day }); // โ Both subscriptions active immediately // โ Customer gets access NOW // โ In 1 day, Stripe charges automatically (no popup) ``` **Timeline**: - **Day 0 (now)**: Customer authenticates card (1 popup) โ Both subscriptions active โ Customer gets access - **Day 1**: Stripe charges automatically (no popup needed) **โ ๏ธ Business Considerations**: - Customer is charged tomorrow, not today - Revenue recognition delayed by 1 day - Customer expects immediate charge when they click "Subscribe" - May need to explain "Payment will be processed within 24 hours" **When NOT to Use**: - โ Customer expects immediate charge confirmation - โ Accounting requires same-day revenue - โ Customer might cancel within 24 hours (before charge) **When to Consider**: - โ Creating 3+ subscriptions (avoid 3+ popups) - โ UX priority (single authentication > immediate charge) - โ Can handle delayed revenue recognition #### 2. Update Subscription Service **File**: `client/app/services/subscription.service.ts` ```typescript import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; @Injectable() export class SubscriptionService { private apiUrl = '/api/subscription'; constructor(private http: HttpClient) {} /** * Pre-authenticate card before creating subscriptions */ setupCard(custId: string, pmId: string): Observable { return this.http.post(`${this.apiUrl}/setupCard`, { custId, pmId }); } /** * Create/update subscriptions */ updateSubscriptions(data: { pmId?: string, package?: string, addons?: any[], defaultPM?: boolean, coupon?: string }): Observable { return this.http.post(`${this.apiUrl}/update`, data); } /** * Check subscription status (for polling after 3DS) */ checkSubscriptionStatus(subscriptionId: string): Observable { return this.http.get(`${this.apiUrl}/status/${subscriptionId}`); } } ``` #### 3. Update Subscription Component **File**: `client/app/components/subscription/subscription.component.ts` ```typescript import { Component } from '@angular/core'; import { SubscriptionService } from '../../services/subscription.service'; import { StripeService } from '../../services/stripe.service'; @Component({ selector: 'app-subscription', templateUrl: './subscription.component.html' }) export class SubscriptionComponent { loading = false; loadingMessage = ''; customerId: string; // From user session/auth constructor( private subscriptionService: SubscriptionService, private stripeService: StripeService ) {} /** * Create subscriptions with Setup Intent pattern * Recommended for package + addons */ async createSubscriptionsWithSetupIntent( packageId: string, addons: any[], pmId: string, couponCode?: string ) { this.loading = true; this.loadingMessage = 'Verifying payment method...'; try { // STEP 1: Pre-authenticate card console.log('Authenticating card...'); const setupResult = await this.subscriptionService .setupCard(this.customerId, pmId) .toPromise(); // STEP 2: Handle 3DS if required if (setupResult.requiresAction && setupResult.clientSecret) { console.log('3DS authentication required...'); this.loadingMessage = 'Please complete authentication...'; const setupIntent = await this.stripeService.confirmCardSetup( setupResult.clientSecret ); if (setupIntent.status !== 'succeeded') { throw new Error('Card authentication failed or was cancelled'); } console.log('Card authenticated successfully via 3DS'); } else { console.log('Card authenticated (no 3DS required)'); } // STEP 3: Create all subscriptions with authenticated card this.loadingMessage = 'Creating subscriptions...'; console.log('Creating subscriptions with authenticated card...'); const subscriptions = await this.subscriptionService .updateSubscriptions({ pmId: pmId, package: packageId, addons: addons, defaultPM: true, coupon: couponCode }) .toPromise(); // Success! console.log('All subscriptions created successfully'); this.handleSuccess(subscriptions); } catch (error) { console.error('Subscription error:', error); this.handleError(error); } finally { this.loading = false; this.loadingMessage = ''; } } private handleSuccess(subscriptions: any) { alert('Subscriptions created successfully!'); // Navigate to dashboard or show success message } private handleError(error: any) { let message = 'Failed to create subscriptions'; if (error.message?.includes('authentication')) { message = 'Card authentication failed. Please try again.'; } else if (error.message?.includes('cancelled')) { message = 'Authentication was cancelled. Please try again.'; } else if (error.error?.error?.message) { message = error.error.error.message; } alert('Error: ' + message); } } ``` --- ## ๐จ Error Handling Flow Diagram ```mermaid flowchart TD Start[Subscription Request] --> CreateSub[Backend Creates Subscription] CreateSub --> Check{PaymentStatus?} Check -->|requires_action| Return3DS[Return client_secret] Check -->|succeeded| Success[Subscription Active โ] Check -->|requires_payment_method| Failed[Payment Failed] Return3DS --> Show[Show 3DS Popup] Show --> User3DS{UserAction?} User3DS -->|Completes| Confirm[confirmCardPayment] User3DS -->|Cancels| Cancel[User Cancelled] User3DS -->|Timeout| Timeout[3DS Timeout] Confirm --> Result{PaymentResult?} Result -->|succeeded| Auto[Stripe Auto-UpdatesSubscription to Active] Result -->|failed| Declined[Card Declined] Auto --> Webhook[Webhook Updates DB] Webhook --> Email[Send Confirmation Email] Email --> FinalSuccess[Show Success Message โ] Cancel --> ShowCancel[Show: Authentication CancelledTry Again] Timeout --> ShowTimeout[Show: Authentication TimeoutTry Again] Declined --> ShowDeclined[Show: Card DeclinedTry Different Card] Failed --> ShowFailed[Show: Payment FailedCheck Card Details] ShowCancel --> Retry{UserRetries?} ShowTimeout --> Retry ShowDeclined --> Retry ShowFailed --> Retry Retry -->|YES| Start Retry -->|NO| End[Subscription Incomplete] style Success fill:#d4edda style FinalSuccess fill:#d4edda style Failed fill:#f8d7da style Cancel fill:#fff3cd style Timeout fill:#fff3cd style Declined fill:#f8d7da style End fill:#f8d7da ``` ### Error Scenarios & Frontend Responses ```mermaid graph TB subgraph "Error Type" E1[Card Declined] E2[User Cancelled 3DS] E3[3DS Timeout] E4[Network Error] E5[Card Requires ActionBut No client_secret] end subgraph "User Message" M1[Your card was declined.Please try a different card.] M2[Authentication was cancelled.Click Subscribe to try again.] M3[Authentication timed out.Please try again.] M4[Network error occurred.Please check connection and retry.] M5[Payment processing error.Please contact support.] end subgraph "Action" A1[Allow retry withdifferent card] A2[Allow retry withsame card] A3[Allow retry] A4[Retry button] A5[Contact support button] end E1 --> M1 --> A1 E2 --> M2 --> A2 E3 --> M3 --> A3 E4 --> M4 --> A4 E5 --> M5 --> A5 style E1 fill:#f8d7da style E2 fill:#fff3cd style E3 fill:#fff3cd style E4 fill:#e7f3ff style E5 fill:#f8d7da ``` --- ``` #### 4. HTML Template with Loading States **File**: `client/app/components/subscription/subscription.component.html` ```html Subscribe Essential - $10/month Professional - $20/month {{addon.name}} - ${{addon.price}}/month {{pm.card.brand}} ending in {{pm.card.last4}} Subscribe Now {{loadingMessage || 'Processing...'}} ๐ Please complete authentication in the popup window You may see a verification screen from your bank ``` ```typescript // Component method onSubscribe() { const selectedAddons = this.availableAddons .filter(a => a.selected) .map(a => ({ price: a.priceKey, quantity: 1 })); this.createSubscriptionsWithSetupIntent( this.selectedPackage, selectedAddons, this.selectedPaymentMethod, this.couponCode ); } ``` ### Testing Setup Intent Pattern **Test Card Numbers**: - `4242424242424242` - No 3DS (immediate success) - `4000000000003220` - 3DS required (popup shown) - `4000000000000341` - Always fails (error handling) **Test Script**: ```bash # Create test script: test_setup_intent.js node test_setup_intent.js ``` **Expected Behavior**: 1. **Regular Card**: No popup, subscriptions created immediately 2. **3DS Card**: One popup, customer authenticates, all subscriptions created 3. **Failed Card**: Clear error message, no subscriptions created ### Flow Comparison **Without Setup Intent** (Old): ``` 1. Create package subscription โ 3DS popup 2. (If 3DS not completed) Create addon subscription โ Fails 3. Result: Package active, Addon missing โ ``` **With Setup Intent** (New): ``` 1. Setup card authentication โ 3DS popup (if needed) 2. Create package subscription โ No popup โ 3. Create addon subscription โ No popup โ 4. Result: Both subscriptions active โ ``` --- ## ๐ฏ Direct Subscription Pattern (For Single Subscriptions) This is the EXISTING implementation for handling 3DS during subscription creation. ### ๐ Flow Diagram: Direct Subscription with 3DS ```mermaid sequenceDiagram participant User participant Frontend participant Backend participant Stripe participant Bank User->>Frontend: Click "Subscribe" Frontend->>Backend: POST /api/subscription/update{package, pmId} Backend->>Stripe: Create Subscriptionpayment_behavior: default_incomplete Stripe->>Stripe: Create PaymentIntent alt Card Requires 3DS Stripe-->>Backend: Subscription: incompletePaymentIntent: requires_action Backend->>Backend: handleSubscriptionPayment() Backend-->>Frontend: {requiresAction: true,client_secret, subscription} Frontend->>User: Show 3DS Popup Frontend->>Stripe: confirmCardPayment(client_secret) Stripe->>Bank: Request Authentication Bank->>User: Show 3DS Challenge User->>Bank: Complete Authentication Bank-->>Stripe: Authentication Success Stripe-->>Frontend: PaymentIntent: succeeded Note over Frontend: โ ๏ธ Subscription still 'incomplete'! Frontend->>Frontend: Poll subscription status(every 2 seconds) Stripe->>Stripe: Auto-charge card (background) Stripe->>Stripe: Update subscription: active Stripe->>Backend: Webhook: customer.subscription.updated Backend->>Backend: Update DB, Send Email Frontend->>Backend: GET /subscription/status Backend-->>Frontend: Subscription: active Frontend->>User: Show Success โ else No 3DS Required Stripe->>Stripe: Charge card immediately Stripe-->>Backend: Subscription: active Backend-->>Frontend: {subscription: active} Frontend->>User: Show Success โ end ``` ### ๐ What Happens Automatically After 3DS? ```mermaid flowchart LR A[Customer Completes 3DS] --> B[Stripe Receives Auth] B --> C[Stripe Auto-Charges Card] C --> D{PaymentSuccessful?} D -->|YES| E[Stripe Updates Subscriptionincomplete โ active] D -->|NO| F[Subscription Stays Incomplete] E --> G[Stripe Sends Webhookcustomer.subscription.updated] G --> H[Backend Updates DB] H --> I[Backend Sends Email] F --> J[User Sees ErrorAdd Different Card] style C fill:#e1f5e1 style E fill:#d4edda style H fill:#d4edda style F fill:#f8d7da style J fill:#f8d7da ``` **Key Point**: After customer completes 3DS, **NO additional frontend action needed** - Stripe handles everything automatically! ### Backend Implementation **Location**: [controllers/subscription.js](../controllers/subscription.js) Line ~1734 ```javascript // After finalizing invoice if (finalizedInvoice.status === 'open' || finalizedInvoice.payment_intent) { let pi = finalizedInvoice.payment_intent ? (typeof finalizedInvoice.payment_intent === 'string' ? await stripe.paymentIntents.retrieve(finalizedInvoice.payment_intent) : finalizedInvoice.payment_intent) : null; let piStatus = pi?.status; debug(`Payment intent initial status: ${piStatus}`); // NEW: Check if payment requires customer action (3DS) if (piStatus === 'requires_action') { debug(`Payment requires customer action (3DS) for subscription ${subscription.id}`); // Return subscription with client_secret for frontend to handle return res.json([{ ...subscription, requires_action: true, client_secret: pi.client_secret, payment_intent_id: pi.id }]); } // EXISTING: Confirm payment intent for regular cards if (piStatus === 'requires_confirmation' && pi) { debug(`Confirming payment intent ${pi.id} to trigger payment attempt...`); try { pi = await stripe.paymentIntents.confirm(pi.id); piStatus = pi.status; debug(`Payment intent confirmed, new status: ${piStatus}`); // NEW: Check again if confirmation resulted in requires_action if (piStatus === 'requires_action') { debug(`Payment confirmation requires customer action (3DS)`); return res.json([{ ...subscription, requires_action: true, client_secret: pi.client_secret, payment_intent_id: pi.id }]); } } catch (confirmErr) { debug(`Payment intent confirmation failed: ${confirmErr.message}`); piStatus = 'requires_payment_method'; } } // EXISTING: Fail only on actual payment failures if (piStatus === 'requires_payment_method') { // ... delete subscription and throw error } } ``` --- ## ๐จ Frontend Changes (Angular 2) ### 1. Install/Import Stripe.js **In `client/index.html`** (if not already added): ```html ``` ### 2. Create Stripe Service **File: `client/app/services/stripe.service.ts`** ```typescript import { Injectable } from '@angular/core'; declare var Stripe: any; @Injectable() export class StripeService { private stripe: any; constructor() { // Initialize with your publishable key from environment this.stripe = Stripe('pk_test_51LlCfSJxyI1MWs2Ty9utAc7QHhAa4YT6VPosvDdFtRaRQJchCLgd4NGvnarZQsCKiQUfJeOmnzs81w0AktP0N1o300Jd4q4m8n'); } /** * Handle 3D Secure authentication * @param clientSecret Payment Intent client_secret from backend * @returns Promise that resolves when authentication completes */ async handleCardAction(clientSecret: string): Promise { try { const result = await this.stripe.handleCardAction(clientSecret); if (result.error) { // Authentication failed throw new Error(result.error.message); } // Authentication succeeded - but subscription is still 'incomplete'! // PaymentIntent is 'succeeded' but subscription needs time to activate return result.paymentIntent; } catch (error) { console.error('3DS authentication error:', error); throw error; } } /** * Confirm payment (alternative method) * @param clientSecret Payment Intent client_secret * @returns Promise with payment intent result */ async confirmCardPayment(clientSecret: string): Promise { try { const result = await this.stripe.confirmCardPayment(clientSecret); if (result.error) { throw new Error(result.error.message); } // Authentication succeeded - but subscription is still 'incomplete'! return result.paymentIntent; } catch (error) { console.error('Payment confirmation error:', error); throw error; } } } ``` ### 3. Update Subscription Component **File: `client/app/components/subscription/subscription.component.ts`** ```typescript import { Component } from '@angular/core'; import { SubscriptionService } from '../../services/subscription.service'; import { StripeService } from '../../services/stripe.service'; @Component({ selector: 'app-subscription', templateUrl: './subscription.component.html' }) export class SubscriptionComponent { loading = false; requires3DS = false; constructor( private subscriptionService: SubscriptionService, private stripeService: StripeService ) {} async createSubscription(packageId: string, couponCode?: string) { this.loading = true; try { // Call backend to create subscription const response = await this.subscriptionService.createSubscription( packageId, couponCode ).toPromise(); // Check if 3DS authentication is required if (response.requires_action && response.client_secret) { this.requires3DS = true; // Handle 3D Secure authentication await this.handle3DSAuthentication(response); } else { // Normal subscription created successfully this.handleSubscriptionSuccess(response); } } catch (error) { this.handleSubscriptionError(error); } finally { this.loading = false; this.requires3DS = false; } } private async handle3DSAuthentication(subscription: any) { try { console.log('Starting 3DS authentication...'); // Use Stripe.js to handle card action (3DS popup) const paymentIntent = await this.stripeService.handleCardAction( subscription.client_secret ); console.log('3DS authentication completed:', paymentIntent.status); // โ ๏ธ CRITICAL: After 3DS completion, PaymentIntent is 'succeeded' // BUT subscription is still 'incomplete'! // Must poll subscription status until it becomes 'active' if (paymentIntent.status === 'succeeded') { // Payment authenticated - now wait for Stripe to charge and activate subscription console.log('โณ Waiting for subscription to activate...'); await this.pollSubscriptionStatus(subscription.id); } else if (paymentIntent.status === 'requires_payment_method') { // Authentication failed throw new Error('3D Secure authentication failed. Please try a different payment method.'); } else { // Other status (processing, etc.) console.log('Payment status:', paymentIntent.status); await this.pollSubscriptionStatus(subscription.id); } } catch (error) { console.error('3DS authentication error:', error); throw new Error('Payment authentication failed: ' + error.message); } } /** * Poll subscription status until it becomes 'active' * Stripe automatically charges and activates after 3DS completion * This usually takes 1-3 seconds */ private async pollSubscriptionStatus(subscriptionId: string, maxAttempts = 10) { let attempts = 0; console.log('๐ Polling subscription status...'); while (attempts < maxAttempts) { await this.delay(2000); // Wait 2 seconds between checks attempts++; const status = await this.subscriptionService.checkSubscriptionStatus( subscriptionId ).toPromise(); console.log(`Poll attempt ${attempts}: status = ${status.status}`); if (status.status === 'active') { console.log('โ Subscription activated!'); this.handleSubscriptionSuccess(status); return; } if (status.status === 'incomplete_expired' || status.status === 'canceled') { throw new Error('Subscription expired or was canceled during payment processing.'); } // Still incomplete or past_due - keep polling if (status.status === 'incomplete' || status.status === 'past_due') { continue; } } // Timeout - subscription didn't become active throw new Error('Subscription payment processing timeout. Please check your subscription status or contact support.'); } private delay(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } private handleSubscriptionSuccess(subscription: any) { console.log('Subscription created successfully:', subscription); // Show success message, redirect, etc. alert('Subscription activated successfully!'); // Navigate to dashboard or subscription page } private handleSubscriptionError(error: any) { console.error('Subscription error:', error); const errorMessage = error.error?.message || error.message || 'Subscription failed'; alert('Error: ' + errorMessage); } } ``` ### 4. Update Subscription Service **File: `client/app/services/subscription.service.ts`** ```typescript import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; @Injectable() export class SubscriptionService { private apiUrl = '/api/subscription'; constructor(private http: HttpClient) {} createSubscription(packageId: string, couponCode?: string): Observable { return this.http.post(`${this.apiUrl}/updateSubscriptions`, { package: packageId, coupon: couponCode }); } checkSubscriptionStatus(subscriptionId: string): Observable { return this.http.get(`${this.apiUrl}/status/${subscriptionId}`); } } ``` ### 5. Add Backend Endpoint for Status Check **File: `controllers/subscription.js`** ```javascript /** * @api {get} /api/subscription/status/:subscriptionId Check Subscription Status * @apiName CheckSubscriptionStatus * @apiGroup Subscription * @apiDescription Check current subscription status (for polling after 3DS) */ exports.checkSubscriptionStatus = async (req, res) => { const { subscriptionId } = req.params; try { const subscription = await stripe.subscriptions.retrieve(subscriptionId); res.json({ id: subscription.id, status: subscription.status, current_period_start: subscription.current_period_start, current_period_end: subscription.current_period_end }); } catch (error) { throw new AppError(Errors.UNKNOWN_APP_ERROR, 'Failed to retrieve subscription status'); } }; ``` **File: `routes/subscription.js`** ```javascript router.get('/api/subscription/status/:subscriptionId', requiresAuth, subscriptionController.checkSubscriptionStatus ); ``` --- ## ๐จ UI/UX Considerations ### Loading States ```html Subscribe Now Creating subscription... Waiting for authentication... ๐ Please complete authentication in the popup window ``` ### Error Handling ```typescript private handleSubscriptionError(error: any) { let message = 'Subscription failed'; if (error.error?.error?.message) { message = error.error.error.message; } else if (error.message) { message = error.message; } // Show user-friendly messages if (message.includes('authentication')) { message = 'Payment authentication was not completed. Please try again.'; } else if (message.includes('payment_method')) { message = 'Payment failed. Please check your card details and try again.'; } this.showErrorNotification(message); } ``` --- ## ๐ Pattern Comparison Diagram ```mermaid graph TB subgraph Direct["Direct Subscription Pattern"] D1[POST /api/subscription/update] D2{3DSRequired?} D3[Show 3DS Popup] D4[Stripe Auto-Charges] D5[Subscription Active โ] D1 --> D2 D2 -->|YES| D3 D2 -->|NO| D5 D3 --> D4 D4 --> D5 end subgraph Setup["Setup Intent Pattern"] S1[POST /api/subscription/setupCard] S2{3DSRequired?} S3[Show 3DS Popup] S4[Card Authenticated] S5[POST /api/subscription/update] S6[Subscriptions Active โ] S1 --> S2 S2 -->|YES| S3 S2 -->|NO| S4 S3 --> S4 S4 --> S5 S5 --> S6 end style D5 fill:#d4edda style S6 fill:#d4edda style D3 fill:#f8d7da style S3 fill:#f8d7da ``` ### When to Use Which Pattern? ```mermaid flowchart TD Start{What are you doing?} Start -->|Creating NEW subscriptionwith immediate charge| DirectUse[Use Direct Pattern] Start -->|Upgrading/Downgradingexisting subscription| DirectUse Start -->|Adding addonwith immediate charge| DirectUse Start -->|Reactivating subscriptioncancel_at_period_end=false| SetupUse[Use Setup Intent] Start -->|Adding card during trialno charge yet| SetupUse Start -->|Changing payment methodnext charge is future| SetupUse DirectUse --> DirectFlow["โ Immediate chargeโ Simple flowโ May require 3DS popupโ Stripe auto-handles after 3DS"] SetupUse --> SetupFlow["โ No immediate chargeโ Pre-authenticate cardโ May require 3DS popupโ Avoids double authentication"] style DirectUse fill:#e1f5e1 style SetupUse fill:#fff3cd style DirectFlow fill:#d4edda style SetupFlow fill:#d4edda ``` --- ## ๐งช Testing ### Test Cards Use these Stripe test cards: - `4242424242424242` - No 3DS (immediate success) - `4000000000003220` - 3DS required (authentication popup) - `4000000000000341` - Always fails (test error handling) ### Test Scenarios #### Scenario 1: Setup Intent Pattern (Multiple Subscriptions) ```typescript // Package: ess_1, Addons: [addon_1, addon_2] // Card: 4000000000003220 (3DS required) // Expected Flow: // 1. Call setupCard โ Returns requiresAction: true // 2. Frontend shows 3DS popup ONCE // 3. Customer authenticates // 4. Create subscriptions โ All succeed without additional popups // 5. Result: Package + 2 addons all active โ ``` #### Scenario 2: Direct Pattern (Single Subscription) ```typescript // Package: ess_1 only // Card: 4000000000003220 (3DS required) // Expected Flow: // 1. Call updateSubscriptions โ Returns requires_action: true // 2. Frontend shows 3DS popup // 3. Customer authenticates // 4. Poll subscription status // 5. Result: Package active โ ``` #### Scenario 3: No 3DS Required ```typescript // Card: 4242424242424242 // Expected Flow (both patterns): // 1. Call setupCard OR updateSubscriptions // 2. No popup shown // 3. Immediate success // 4. Result: All subscriptions active โ ``` #### Scenario 4: Failed Card ```typescript // Card: 4000000000000341 // Expected Flow: // 1. Setup Intent: Authentication fails immediately // 2. Direct: Subscription creation fails // 3. Clear error message shown // 4. Result: No subscriptions created โ ``` ### Test Implementation Create test file: `test_setup_intent_pattern.js` ```javascript // Test Setup Intent Pattern const axios = require('axios'); async function testSetupIntentPattern() { console.log('Testing Setup Intent Pattern...\n'); // Test 1: Regular card (no 3DS) console.log('Test 1: Regular card'); let result = await axios.post('http://localhost:4100/api/subscription/setupCard', { custId: 'cus_test123', pmId: 'pm_card_visa' // 4242 card }); console.log('Result:', result.data); console.log('Expected: requiresAction=false โ \n'); // Test 2: 3DS card console.log('Test 2: 3DS card'); result = await axios.post('http://localhost:4100/api/subscription/setupCard', { custId: 'cus_test123', pmId: 'pm_card_threeDSecure' // 3220 card }); console.log('Result:', result.data); console.log('Expected: requiresAction=true, clientSecret present โ \n'); console.log('All tests passed!'); } testSetupIntentPattern(); ``` --- ## ๐ Implementation Decision Guide ### When to Use Setup Intent Pattern **Use Setup Intent if**: - โ Creating multiple subscriptions (package + addons) - โ Payment method might require 3DS - โ Want to prevent partial subscription creation - โ Need atomic all-or-nothing behavior - โ Better user experience is priority **Example**: Customer subscribing to Professional plan + 2 addons ### When to Use Direct Pattern **Use Direct Pattern if**: - โ Creating single subscription only - โ Quick checkout flow - โ Minimal code changes needed - โ Legacy code compatibility **Example**: Customer subscribing to Essential plan only, no addons ### Comparison Matrix | Feature | Setup Intent Pattern | Direct Pattern | |---------|---------------------|----------------| | **Best For** | Multiple subscriptions | Single subscription | | **3DS Handling** | Pre-authentication | During subscription creation | | **Popups** | 1 popup max | 1 popup per subscription | | **Partial Creation Risk** | โ None | โ ๏ธ Possible | | **User Experience** | โญโญโญโญโญ Excellent | โญโญโญ Good | | **Implementation** | New endpoint + frontend | Existing flow | | **Code Complexity** | Medium | Low | | **Reliability** | โญโญโญโญโญ Excellent | โญโญโญโญ Good | | **SCA Compliant** | โ Yes | โ Yes | ### Migration Path **Phase 1** (Immediate): - Implement Setup Intent endpoint โ - Use for new multi-subscription flows โ **Phase 2** (Gradual): - Update frontend components one by one - Keep Direct Pattern for backward compatibility **Phase 3** (Future): - Migrate all flows to Setup Intent - Remove Direct Pattern 3DS handling --- ## ๐ Summary ### Problem Solved **Before**: - Multiple subscriptions with 3DS card โ Multiple popups - Second subscription fails if first 3DS not complete - Partial subscription creation (package works, addon fails) - Confused customers and lost revenue **After**: - Setup Intent pre-authenticates card once - Single 3DS popup for all subscriptions - Atomic creation (all succeed or none created) - Clear authentication flow ### Implementation Status **Completed**: - โ Backend `/api/subscription/setupCard` endpoint - โ Full JSDoc/apidoc documentation - โ Route configuration - โ Error handling (card errors, invalid requests) - โ Stripe API integration - โ Frontend implementation guide - โ Testing scenarios **Next Steps**: 1. Test endpoint with Postman or test script 2. Update frontend components to use appropriate pattern (Direct vs Setup Intent) 3. Test with all three card types 4. Monitor production for 3DS success rates --- ## ๐ก Additional Recommendations & Best Practices ### 1. Multi-Subscription Creation Strategy **Current Behavior** (Immediate Charge): - Package subscription created first - If 3DS required โ returns `client_secret`, subscription incomplete - Addon subscription NOT created (error interrupts flow) - Frontend completes 3DS for package - Frontend calls `/update` again โ addon subscription created **This is CORRECT** - prevents partial creation. **Optimization Option**: Use trial period to avoid sequential 3DS: ```javascript { "package": "ess_1", "addons": [{"price": "addon_1", "quantity": 1}], "trial_period_days": 1 // Both created without immediate charge } ``` ### 2. Payment Method Validation **Recommendation**: Validate payment method BEFORE subscription creation: ```typescript // Use Setup Intent for validation (no charge) async validatePaymentMethod(custId: string, pmId: string) { const result = await this.setupCardAuthentication(custId, pmId); if (result.requiresAction) { // Show 3DS popup const { error } = await this.stripe.confirmCardSetup(result.clientSecret); if (error) throw error; } return true; // Card validated } // Then create subscriptions with validated card await this.validatePaymentMethod(custId, pmId); await this.updateSubscriptions({ package, addons }); ``` **When to Use**: - User adding new card to profile (no charge yet) - Changing default payment method on active subscription - Before charging for any service ### 3. Error Handling Best Practices **Always check for 3DS requirement**: ```typescript try { const subscriptions = await api.updateSubscriptions(params); // Check if any subscription requires action const requires3DS = subscriptions.some(sub => sub.requires_action); if (requires3DS) { for (const sub of subscriptions) { if (sub.requires_action && sub.client_secret) { // Handle 3DS for this subscription await this.handle3DS(sub.client_secret); } } // Refresh subscriptions after 3DS completion return await api.getSubscriptions(); } return subscriptions; } catch (error) { // Handle errors } ``` ### 4. User Experience Improvements **Show clear messaging**: ```typescript // Before 3DS showMessage('Your bank requires additional verification. Please complete the authentication.'); // During 3DS showSpinner('Authenticating with your bank...'); // After 3DS success showSuccess('Payment method verified! Completing subscription...'); // After subscription complete showSuccess('Subscription activated successfully!'); ``` **Handle edge cases**: - User closes 3DS popup โ Show retry option - 3DS fails โ Clear error message + retry with different card - Network timeout โ Retry mechanism with exponential backoff ### 5. Testing Strategy **Test with Stripe test cards**: ```javascript // No 3DS required '4242424242424242' // 3DS required - authentication succeeds '4000002500003155' // Always fails (declined) '4000000000000341' // 3DS required with specific challenge flow '4000002760003184' ``` **Test scenarios**: 1. โ Single subscription, no 3DS 2. โ Single subscription, 3DS required 3. โ Multiple subscriptions (package + addons), no 3DS 4. โ Multiple subscriptions, 3DS required (sequential authentication) 5. โ Reactivation with new 3DS card (cancel_at_period_end=false) 6. โ Upgrade/downgrade with 3DS card 7. โ User cancels 3DS popup 8. โ Card declined after 3DS completion 9. โ Trial period + 3DS card (no immediate authentication) 10. โ Promo coupon + 3DS card ### 6. Backend Configuration Checklist **Ensure proper payment_behavior** (already implemented): ```javascript // โ CORRECT - in createSubscription() { payment_behavior: 'default_incomplete', // Allows 3DS handling expand: ['latest_invoice.payment_intent'] // Get payment details } ``` **โ AVOID** (Old Implementation - Before Jan 16, 2026): ```javascript { payment_behavior: 'error_if_incomplete' // Throws error on 3DS - BAD! // Problem: 3DS cards throw errors instead of returning client_secret // Fixed: Changed to 'default_incomplete' } ``` ### 7. Monitoring and Analytics **Track 3DS metrics**: - % subscriptions requiring 3DS - % successful 3DS completions - % failed/abandoned 3DS (conversion funnel) - Average time to complete 3DS authentication - 3DS success rate by card type/issuer **Alert on issues**: - High 3DS failure rate (>10%) - Increased incomplete subscriptions - Payment intent confirmation errors - Setup Intent creation failures **Suggested logging**: ```javascript // Log 3DS events for analytics analytics.track('3DS_Required', { subscriptionId, paymentIntentId }); analytics.track('3DS_Started', { subscriptionId }); analytics.track('3DS_Completed', { subscriptionId, success: true }); analytics.track('3DS_Failed', { subscriptionId, error }); analytics.track('3DS_Abandoned', { subscriptionId }); ``` ### 8. Performance Optimizations **Parallel processing where possible**: ```typescript // โ Sequential (slower) await createPackage(); await createAddon(); // โ Parallel when no payment required (trial) await Promise.all([ createPackage({ trial_period_days: 7 }), createAddon({ trial_period_days: 7 }) ]); ``` **Cache validated payment methods**: ```typescript // Store Setup Intent result to avoid re-authentication localStorage.setItem(`pm_validated_${pmId}`, Date.now().toString()); // Check if recently validated (within 1 hour) const validated = localStorage.getItem(`pm_validated_${pmId}`); if (validated && Date.now() - parseInt(validated) < 3600000) { // Skip Setup Intent, card already validated } ``` ### 9. Security Considerations **Never log sensitive data**: ```typescript // โ BAD console.log('Payment method:', paymentMethod); console.log('Client secret:', clientSecret); // โ GOOD console.log('Payment method ID:', paymentMethod.id); console.log('3DS required:', !!clientSecret); ``` **Validate on backend**: - Always verify payment method belongs to customer - Check subscription status after 3DS completion - Validate payment intent status before finalizing --- ## ๐ฏ Quick Reference Summary ### ๐ Visual Cheat Sheet ```mermaid graph LR subgraph Scenarios["Common Scenarios"] S1["New Subscription(immediate charge)"] S2["Upgrade/Downgrade(immediate charge)"] S3["Package + Addons(immediate charge)"] S4["Reactivate(no charge)"] S5["Add Card in Trial(no charge)"] end subgraph Patterns["Use Pattern"] P1[Direct Pattern] P2[Setup Intent] end subgraph Results["3DS Handling"] R1["1 popup per subscription(if 3DS required)"] R2["1 popup total(if 3DS required)"] end S1 --> P1 S2 --> P1 S3 --> P1 S4 --> P2 S5 --> P2 P1 --> R1 P2 --> R2 style P1 fill:#e1f5e1 style P2 fill:#fff3cd style R1 fill:#d4edda style R2 fill:#d4edda ``` ### Decision Table | Scenario | Pattern | Authentication Timing | Popups | |----------|---------|----------------------|--------| | New subscription, immediate charge | Direct | During subscription creation | 0-1 | | Upgrade/downgrade | Direct | During subscription update | 0-1 | | Package + addons, immediate | Direct | Sequential per subscription | 0-2 | | Reactivate with new card | Setup Intent | Before updating cancel_at_period_end | 0-1 | | Add card during trial | Setup Intent | Before trial ends | 0-1 | | Trial period subscription | Setup Intent | After trial, before first charge | 0-1 | | Change payment method | Setup Intent | Before next billing cycle | 0-1 | **Note**: "0-1 popups" means 0 for regular cards, 1 for 3DS cards (~5-15% of cards) ### Implementation Checklist ```mermaid graph TD Start[Start Implementation] --> Q1{ImmediateCharge?} Q1 -->|YES| Direct[Implement Direct Pattern] Q1 -->|NO| Setup[Implement Setup Intent] Direct --> D1[1. Call POST /update] D1 --> D2[2. Check response.requiresAction] D2 --> D3[3. If true: confirmCardPayment] D3 --> D4[4. Wait for auto-activation] D4 --> Done1[โ Done] Setup --> S1[1. Call POST /setupCard] S1 --> S2[2. Check response.requiresAction] S2 --> S3[3. If true: confirmCardSetup] S3 --> S4[4. Call POST /update] S4 --> Done2[โ Done] style Done1 fill:#d4edda style Done2 fill:#d4edda ``` ### Test Card Quick Reference | Card Number | 3DS Required | Result | Use For | |-------------|--------------|--------|---------| | 4242424242424242 | โ No | Always succeeds | Happy path testing | | 4000002500003155 | โ Yes | Succeeds after 3DS | 3DS flow testing | | 4000000000003220 | โ Yes | Succeeds after 3DS | Alternative 3DS test | | 4000000000000341 | โ No | Always declines | Error handling testing | **Next Steps**: 1. Test endpoint with Postman or test script 2. Update frontend components to use Setup Intent 3. Test with all three card types 4. Monitor production for 3DS success rates ### Key Benefits - โ **No Partial Subscriptions**: Either all created or none - โ **Better UX**: Single authentication step - โ **SCA Compliant**: Meets European Strong Customer Authentication - โ **Future-Proof**: Works with upcoming payment regulations - โ **Reusable**: Can apply to other multi-charge scenarios ### Related Documentation - [PAYMENT_FAILURE_HANDLING.md](PAYMENT_FAILURE_HANDLING.md) - Payment failure strategies - [SUBSCRIPTION_PROMO_INTEGRATION.md](SUBSCRIPTION_PROMO_INTEGRATION.md) - Promo handling - [controllers/subscription.js](../controllers/subscription.js) - Backend implementation
๐ Please complete authentication in the popup window
You may see a verification screen from your bank