From 1e165d395c9a5e9ea3e49ab4d2c606467e8b045a Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Sat, 7 Mar 2026 10:10:55 +0530 Subject: [PATCH] feat: Implement Stripe subscription management with dynamic display of payment success and subscription details. --- .../settings/PaymentSuccessForm.tsx | 70 +++++++-- src/components/settings/SubscriptionForm.tsx | 138 +++++++++++++++++- src/services/api.ts | 1 + src/services/index.ts | 4 + src/services/stripe.service.ts | 76 ++++++++++ 5 files changed, 265 insertions(+), 24 deletions(-) create mode 100644 src/services/stripe.service.ts diff --git a/src/components/settings/PaymentSuccessForm.tsx b/src/components/settings/PaymentSuccessForm.tsx index 29eba57..466bcdb 100644 --- a/src/components/settings/PaymentSuccessForm.tsx +++ b/src/components/settings/PaymentSuccessForm.tsx @@ -1,22 +1,59 @@ 'use client'; import Image from 'next/image'; -import { useRouter } from 'next/navigation'; - -interface TransactionDetail { - label: string; - value: string; - bold?: boolean; -} - -const TRANSACTION_DETAILS: TransactionDetail[] = [ - { label: 'Transaction ID', value: 'REQ-89234475', bold: true }, - { label: 'Amount Paid', value: '$499.00', bold: true }, - { label: 'Date', value: 'Oct 24, 2024', bold: true }, -]; +import { useRouter, useSearchParams } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { stripeService, AgentSubscription } from '@/services/stripe.service'; export function PaymentSuccessForm() { const router = useRouter(); + const searchParams = useSearchParams(); + const sessionId = searchParams.get('session_id'); + + const [subscription, setSubscription] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + loadSubscription(); + }, []); + + const loadSubscription = async () => { + try { + const sub = await stripeService.getSubscription(); + setSubscription(sub); + } catch (error) { + console.error('Failed to load subscription:', error); + } finally { + setLoading(false); + } + }; + + const formatDate = (dateStr: string | null | undefined) => { + if (!dateStr) return 'N/A'; + return new Date(dateStr).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); + }; + + const formatAmount = (amount: number) => { + return `$${(amount / 100).toFixed(2)}`; + }; + + if (loading) { + return ( +
+
+
+ ); + } + + const transactionDetails = [ + { label: 'Transaction ID', value: sessionId ? sessionId.slice(0, 16) + '...' : 'N/A', bold: true }, + { label: 'Amount Paid', value: subscription?.plan ? formatAmount(subscription.plan.amount) : '$499.00', bold: true }, + { label: 'Date', value: formatDate(subscription?.createdAt || new Date().toISOString()), bold: true }, + ]; return ( <> @@ -74,7 +111,7 @@ export function PaymentSuccessForm() { {/* Detail Rows */}
- {TRANSACTION_DETAILS.map((detail) => ( + {transactionDetails.map((detail) => (
{detail.label} @@ -95,7 +132,7 @@ export function PaymentSuccessForm() { Next Renewal Date - Oct 24, 2025 + {formatDate(subscription?.currentPeriodEnd)}
@@ -114,6 +151,7 @@ export function PaymentSuccessForm() { Go To Dashboard
diff --git a/src/components/settings/SubscriptionForm.tsx b/src/components/settings/SubscriptionForm.tsx index 798a0f2..106f5a3 100644 --- a/src/components/settings/SubscriptionForm.tsx +++ b/src/components/settings/SubscriptionForm.tsx @@ -2,7 +2,8 @@ import Image from 'next/image'; import Link from 'next/link'; -import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { stripeService, AgentSubscription } from '@/services/stripe.service'; interface PlanFeature { icon: string; @@ -17,7 +18,81 @@ const PLAN_FEATURES: PlanFeature[] = [ ]; export function SubscriptionForm() { - const router = useRouter(); + const [subscription, setSubscription] = useState(null); + const [loading, setLoading] = useState(true); + const [actionLoading, setActionLoading] = useState(false); + + useEffect(() => { + loadSubscription(); + }, []); + + const loadSubscription = async () => { + try { + const subData = await stripeService.getSubscription(); + setSubscription(subData); + } catch (error) { + console.error('Failed to load subscription data:', error); + } finally { + setLoading(false); + } + }; + + const handleGetPremium = async () => { + setActionLoading(true); + try { + // Fetch plan ID on-demand when user clicks the button + const plans = await stripeService.getPlans(); + if (!plans.length) { + alert('No subscription plans available. Please try again later.'); + return; + } + const { url } = await stripeService.createCheckoutSession(plans[0].id); + window.location.href = url; + } catch (error: any) { + console.error('Failed to create checkout session:', error); + alert(error.response?.data?.message || 'Failed to start checkout. Please try again.'); + } finally { + setActionLoading(false); + } + }; + + const handleManageSubscription = async () => { + setActionLoading(true); + try { + const { url } = await stripeService.createPortalSession(); + window.location.href = url; + } catch (error) { + console.error('Failed to open billing portal:', error); + } finally { + setActionLoading(false); + } + }; + + const handleCancelSubscription = async () => { + if (!confirm('Are you sure you want to cancel your subscription? You will retain access until the end of your billing period.')) { + return; + } + setActionLoading(true); + try { + await stripeService.cancelSubscription(); + await loadSubscription(); + } catch (error) { + console.error('Failed to cancel subscription:', error); + } finally { + setActionLoading(false); + } + }; + + const formatDate = (dateStr: string | null) => { + if (!dateStr) return ''; + return new Date(dateStr).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }); + }; + + const isActive = subscription?.status === 'ACTIVE'; return ( <> @@ -31,6 +106,46 @@ export function SubscriptionForm() {

+ {/* Active Subscription Status */} + {isActive && ( +
+
+
+

+ Active Subscription +

+
+

+ Plan: {subscription.plan?.name} +

+ {subscription.currentPeriodEnd && ( +

+ {subscription.cancelAtPeriodEnd + ? `Cancels on: ${formatDate(subscription.currentPeriodEnd)}` + : `Renews on: ${formatDate(subscription.currentPeriodEnd)}`} +

+ )} +
+ + {!subscription.cancelAtPeriodEnd && ( + + )} +
+
+ )} + {/* Annual Membership Plans */}

@@ -74,12 +189,19 @@ export function SubscriptionForm() {

{/* CTA Button */} - + {isActive ? ( + + Current Plan + + ) : ( + + )}
{/* Right Side - Features + Illustration */} diff --git a/src/services/api.ts b/src/services/api.ts index 72e4d1c..bdaccdb 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -95,6 +95,7 @@ const publicEndpoints = [ '/auth/2fa/verify-backup', '/agent-types', '/cms', + '/stripe/plans', ]; // Check if URL is a public endpoint diff --git a/src/services/index.ts b/src/services/index.ts index 1132900..94f098e 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -63,3 +63,7 @@ export { cmsService } from './cms.service'; export { notificationService } from './notification.service'; export { notificationsApiService } from './notifications-api.service'; export type { AppNotification } from './notifications-api.service'; + +// Stripe Service +export { stripeService } from './stripe.service'; +export type { SubscriptionPlan, AgentSubscription, PaymentRecord } from './stripe.service'; diff --git a/src/services/stripe.service.ts b/src/services/stripe.service.ts new file mode 100644 index 0000000..588ff7d --- /dev/null +++ b/src/services/stripe.service.ts @@ -0,0 +1,76 @@ +import api from './api'; + +export interface SubscriptionPlan { + id: string; + name: string; + description: string | null; + stripePriceId: string; + amount: number; + currency: string; + interval: string; + features: string | null; + isActive: boolean; + sortOrder: number; +} + +export interface AgentSubscription { + id: string; + userId: string; + planId: string; + stripeCustomerId: string; + stripeSubscriptionId: string | null; + status: string; + currentPeriodStart: string | null; + currentPeriodEnd: string | null; + cancelAtPeriodEnd: boolean; + canceledAt: string | null; + plan: SubscriptionPlan; +} + +export interface PaymentRecord { + id: string; + subscriptionId: string | null; + userId: string; + stripePaymentIntentId: string | null; + stripeInvoiceId: string | null; + amount: number; + currency: string; + status: string; + receiptUrl: string | null; + createdAt: string; + subscription: AgentSubscription | null; +} + +class StripeApiService { + async getPlans(): Promise { + const response = await api.get('/stripe/plans'); + return response.data.data; + } + + async getSubscription(): Promise { + const response = await api.get('/stripe/subscription'); + return response.data.data; + } + + async createCheckoutSession(planId: string): Promise<{ url: string }> { + const response = await api.post('/stripe/create-checkout-session', { planId }); + return response.data.data; + } + + async createPortalSession(): Promise<{ url: string }> { + const response = await api.post('/stripe/create-portal-session', {}); + return response.data.data; + } + + async cancelSubscription(): Promise { + const response = await api.post('/stripe/cancel-subscription'); + return response.data.data; + } + + async getPaymentHistory(): Promise { + const response = await api.get('/stripe/payments'); + return response.data.data; + } +} + +export const stripeService = new StripeApiService();