From fe2b1bbe39b1212cf007e14611cdf9ec1fc85887 Mon Sep 17 00:00:00 2001 From: javayhu Date: Sat, 5 Jul 2025 22:30:22 +0800 Subject: [PATCH] feat: add credit purchase functionality with Stripe integration - Introduced credit purchase payment intent actions in credits.action.ts. - Created new components for credit packages and Stripe payment form. - Added routes and layout for credits settings page. - Updated sidebar configuration to include credits settings. - Enhanced constants for credit packages with detailed pricing and descriptions. - Implemented loading and layout components for credits page. - Integrated payment confirmation handling in Stripe provider. --- env.example | 1 + messages/en.json | 7 + src/actions/credits.action.ts | 87 +++++++ .../(protected)/settings/credits/layout.tsx | 46 ++++ .../(protected)/settings/credits/loading.tsx | 5 + .../(protected)/settings/credits/page.tsx | 9 + .../settings/credits/credit-packages.tsx | 216 ++++++++++++++++++ .../settings/credits/stripe-payment-form.tsx | 191 ++++++++++++++++ src/config/sidebar-config.tsx | 7 + src/lib/constants.ts | 35 ++- src/payment/index.ts | 27 +++ src/payment/provider/stripe.ts | 102 +++++++++ src/payment/types.ts | 34 +++ src/routes.ts | 2 + 14 files changed, 764 insertions(+), 5 deletions(-) create mode 100644 src/app/[locale]/(protected)/settings/credits/layout.tsx create mode 100644 src/app/[locale]/(protected)/settings/credits/loading.tsx create mode 100644 src/app/[locale]/(protected)/settings/credits/page.tsx create mode 100644 src/components/settings/credits/credit-packages.tsx create mode 100644 src/components/settings/credits/stripe-payment-form.tsx diff --git a/env.example b/env.example index e158843..7ecd61a 100644 --- a/env.example +++ b/env.example @@ -63,6 +63,7 @@ STORAGE_PUBLIC_URL="" # https://mksaas.com/docs/payment#setup # Get Stripe key and secret from https://dashboard.stripe.com # ----------------------------------------------------------------------------- +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="" STRIPE_SECRET_KEY="" STRIPE_WEBHOOK_SECRET="" # Pro plan - monthly subscription diff --git a/messages/en.json b/messages/en.json index 7429307..b4156ec 100644 --- a/messages/en.json +++ b/messages/en.json @@ -556,6 +556,13 @@ "retry": "Retry", "errorMessage": "Failed to get data" }, + "credits": { + "title": "Credits", + "description": "Manage your credits", + "credits": "Credits", + "creditsDescription": "You have {credits} credits", + "creditsExpired": "Credits expired" + }, "notification": { "title": "Notification", "description": "Manage your notification preferences", diff --git a/src/actions/credits.action.ts b/src/actions/credits.action.ts index 406eefa..0b96e67 100644 --- a/src/actions/credits.action.ts +++ b/src/actions/credits.action.ts @@ -1,12 +1,18 @@ +'use server'; + import { addMonthlyFreeCredits, addRegisterGiftCredits, consumeCredits, getUserCredits, + addCredits, } from '@/lib/credits'; import { getSession } from '@/lib/server'; import { createSafeActionClient } from 'next-safe-action'; import { z } from 'zod'; +import { createPaymentIntent, confirmPaymentIntent } from '@/payment'; +import { CREDIT_PACKAGES } from '@/lib/constants'; +import { revalidatePath } from 'next/cache'; const actionClient = createSafeActionClient(); @@ -57,3 +63,84 @@ export const consumeCreditsAction = actionClient return { success: false, error: (e as Error).message }; } }); + +// Credit purchase payment intent action +const createPaymentIntentSchema = z.object({ + packageId: z.string().min(1), +}); + +export const createCreditPaymentIntent = actionClient + .schema(createPaymentIntentSchema) + .action(async ({ parsedInput }) => { + const session = await getSession(); + if (!session) return { success: false, error: 'User not authenticated' }; + + const { packageId } = parsedInput; + + // Find the credit package + const creditPackage = CREDIT_PACKAGES.find((pkg) => pkg.id === packageId); + if (!creditPackage) { + return { success: false, error: 'Invalid credit package' }; + } + + try { + // Create payment intent + const paymentIntent = await createPaymentIntent({ + amount: creditPackage.price * 100, // Convert to cents + currency: 'usd', + metadata: { + packageId, + userId: session.user.id, + credits: creditPackage.credits.toString(), + }, + }); + + return { + success: true, + clientSecret: paymentIntent.clientSecret, + }; + } catch (error) { + console.error('Create credit payment intent error:', error); + return { success: false, error: 'Failed to create payment intent' }; + } + }); + +// Confirm credit payment action +const confirmPaymentSchema = z.object({ + packageId: z.string().min(1), + paymentIntentId: z.string().min(1), +}); + +export const confirmCreditPayment = actionClient + .schema(confirmPaymentSchema) + .action(async ({ parsedInput }) => { + const session = await getSession(); + if (!session) return { success: false, error: 'User not authenticated' }; + + const { packageId, paymentIntentId } = parsedInput; + + // Find the credit package + const creditPackage = CREDIT_PACKAGES.find((pkg) => pkg.id === packageId); + if (!creditPackage) { + return { success: false, error: 'Invalid credit package' }; + } + + try { + // Confirm payment intent + const isSuccessful = await confirmPaymentIntent({ + paymentIntentId, + }); + + if (!isSuccessful) { + return { success: false, error: 'Payment confirmation failed' }; + } + + // Revalidate the credits page to show updated balance + revalidatePath('/settings/credits'); + + return { success: true }; + } catch (error) { + console.error('Confirm credit payment error:', error); + return { success: false, error: 'Failed to confirm payment' }; + } + }); diff --git a/src/app/[locale]/(protected)/settings/credits/layout.tsx b/src/app/[locale]/(protected)/settings/credits/layout.tsx new file mode 100644 index 0000000..9fc5771 --- /dev/null +++ b/src/app/[locale]/(protected)/settings/credits/layout.tsx @@ -0,0 +1,46 @@ +import { DashboardHeader } from '@/components/dashboard/dashboard-header'; +import { getTranslations } from 'next-intl/server'; + +interface CreditsLayoutProps { + children: React.ReactNode; +} + +export default async function CreditsLayout({ children }: CreditsLayoutProps) { + const t = await getTranslations('Dashboard.settings'); + + const breadcrumbs = [ + { + label: t('title'), + isCurrentPage: false, + }, + { + label: t('credits.title'), + isCurrentPage: true, + }, + ]; + + return ( + <> + + +
+
+
+
+
+

+ {t('credits.title')} +

+

+ {t('credits.description')} +

+
+ + {children} +
+
+
+
+ + ); +} diff --git a/src/app/[locale]/(protected)/settings/credits/loading.tsx b/src/app/[locale]/(protected)/settings/credits/loading.tsx new file mode 100644 index 0000000..ebfad58 --- /dev/null +++ b/src/app/[locale]/(protected)/settings/credits/loading.tsx @@ -0,0 +1,5 @@ +import { Loader2Icon } from 'lucide-react'; + +export default function Loading() { + return ; +} diff --git a/src/app/[locale]/(protected)/settings/credits/page.tsx b/src/app/[locale]/(protected)/settings/credits/page.tsx new file mode 100644 index 0000000..6d0b763 --- /dev/null +++ b/src/app/[locale]/(protected)/settings/credits/page.tsx @@ -0,0 +1,9 @@ +import { CreditPackages } from '@/components/settings/credits/credit-packages'; + +export default function CreditsPage() { + return ( +
+ +
+ ); +} diff --git a/src/components/settings/credits/credit-packages.tsx b/src/components/settings/credits/credit-packages.tsx new file mode 100644 index 0000000..783c311 --- /dev/null +++ b/src/components/settings/credits/credit-packages.tsx @@ -0,0 +1,216 @@ +'use client'; + +import { createCreditPaymentIntent, getCreditsAction } from '@/actions/credits.action'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { CREDIT_PACKAGES } from '@/lib/constants'; +import { formatPrice } from '@/lib/formatter'; +import { cn } from '@/lib/utils'; +import { CheckIcon, CoinsIcon, Loader2Icon } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { Separator } from '../../ui/separator'; +import { StripePaymentForm } from './stripe-payment-form'; +import { toast } from 'sonner'; + +export function CreditPackages() { + const [loadingPackage, setLoadingPackage] = useState(null); + const [paymentDialog, setPaymentDialog] = useState<{ + isOpen: boolean; + clientSecret: string | null; + packageId: string | null; + }>({ + isOpen: false, + clientSecret: null, + packageId: null, + }); + const [credits, setCredits] = useState(null); + const [loading, setLoading] = useState(true); + + const fetchCredits = async () => { + try { + setLoading(true); + const result = await getCreditsAction(); + if (result?.data?.success) { + console.log('CreditPackages, fetched credits:', result.data.credits); + setCredits(result.data.credits || 0); + } else { + const errorMessage = result?.data?.error || 'Failed to fetch credits'; + console.error('CreditPackages, failed to fetch credits:', errorMessage); + toast.error(errorMessage); + } + } catch (error) { + console.error('CreditPackages, failed to fetch credits:', error); + toast.error('Failed to fetch credits'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchCredits(); + }, []); + + const handlePurchase = async (packageId: string) => { + try { + setLoadingPackage(packageId); + + const result = await createCreditPaymentIntent({ packageId }); + if (result?.data?.success && result?.data?.clientSecret) { + setPaymentDialog({ + isOpen: true, + clientSecret: result.data.clientSecret, + packageId, + }); + } else { + const errorMessage = result?.data?.error || 'Failed to create payment intent'; + console.error('CreditPackages, failed to create payment intent:', errorMessage); + toast.error(errorMessage); + } + } catch (error) { + console.error('CreditPackages, failed to initiate payment:', error); + toast.error('Failed to initiate payment'); + } finally { + setLoadingPackage(null); + } + }; + + const handlePaymentSuccess = () => { + console.log('CreditPackages, payment successful'); + setPaymentDialog({ + isOpen: false, + clientSecret: null, + packageId: null, + }); + + // Refresh credit balance without page reload + fetchCredits(); + + // Show success toast + toast.success('Your credits have been added to your account'); + }; + + const handlePaymentCancel = () => { + console.log('CreditPackages, payment cancelled'); + setPaymentDialog({ + isOpen: false, + clientSecret: null, + packageId: null, + }); + }; + + const getPackageInfo = (packageId: string) => { + return CREDIT_PACKAGES.find((pkg) => pkg.id === packageId); + }; + + return ( +
+ + + Credit Balance + + +
+
+ +
+ {loading ? ( + ... + ) : ( + credits?.toLocaleString() || 0 + )} +
+
+ + + +
+
+

Credit Packages

+

+ Purchase additional credits to use our services +

+
+
+ {CREDIT_PACKAGES.map((pkg) => ( + + {pkg.popular && ( +
+ + Most Popular + +
+ )} + + {/* + {pkg.id} + */} + + + {/* Price and Credits - Left/Right Layout */} +
+
+
+ {pkg.credits.toLocaleString()} +
+
+
+
+ {formatPrice(pkg.price, 'USD')} +
+
+
+ +
+ + {pkg.description} +
+ + {/* purchase button */} + +
+
+ ))} +
+
+
+
+
+ + {/* Payment Dialog */} + + + + Complete Your Purchase + + + {paymentDialog.clientSecret && paymentDialog.packageId && ( + + )} + + +
+ ); +} diff --git a/src/components/settings/credits/stripe-payment-form.tsx b/src/components/settings/credits/stripe-payment-form.tsx new file mode 100644 index 0000000..f86dc00 --- /dev/null +++ b/src/components/settings/credits/stripe-payment-form.tsx @@ -0,0 +1,191 @@ +'use client'; + +import { confirmCreditPayment } from '@/actions/credits.action'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { formatPrice } from '@/lib/formatter'; +import { + Elements, + PaymentElement, + useElements, + useStripe, +} from '@stripe/react-stripe-js'; +import { loadStripe } from '@stripe/stripe-js'; +import { CoinsIcon, Loader2Icon } from 'lucide-react'; +import { useTheme } from 'next-themes'; +import { useMemo, useState } from 'react'; +import { toast } from 'sonner'; + +interface StripePaymentFormProps { + clientSecret: string; + packageId: string; + packageInfo: { + credits: number; + price: number; + description: string; + }; + onPaymentSuccess: () => void; + onPaymentCancel: () => void; +} + +export function StripePaymentForm(props: StripePaymentFormProps) { + const { resolvedTheme: theme } = useTheme(); + const stripePromise = useMemo(() => { + if (!process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY) { + throw new Error('NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is not set'); + } + return loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY); + }, []); + + const options = useMemo(() => ({ + clientSecret: props.clientSecret, + appearance: { + theme: (theme === "dark" ? "night" : "stripe") as "night" | "stripe", + }, + loader: 'auto' as const, + }), [props.clientSecret, theme]); + + return ( + + + + ); +} + +interface PaymentFormProps { + clientSecret: string; + packageId: string; + packageInfo: { + credits: number; + price: number; + description: string; + }; + onPaymentSuccess: () => void; + onPaymentCancel: () => void; +} + +function PaymentForm({ + clientSecret, + packageId, + packageInfo, + onPaymentSuccess, + onPaymentCancel, +}: PaymentFormProps) { + const stripe = useStripe(); + const elements = useElements(); + const [processing, setProcessing] = useState(false); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + + if (!stripe || !elements) { + console.error('Stripe or elements not found'); + return; + } + + setProcessing(true); + + try { + // Confirm the payment using PaymentElement + const { error } = await stripe.confirmPayment({ + elements, + redirect: "if_required", + }); + + if (error) { + console.error('PaymentForm, payment error:', error); + throw new Error(error.message || "Payment failed"); + } else { + // The payment was successful + const paymentIntent = await stripe.retrievePaymentIntent(clientSecret); + if (paymentIntent.paymentIntent) { + const result = await confirmCreditPayment({ + packageId, + paymentIntentId: paymentIntent.paymentIntent.id, + }); + + if (result?.data?.success) { + console.log('PaymentForm, payment success'); + toast.success(`${packageInfo.credits} credits have been added to your account.`); + onPaymentSuccess(); + } else { + console.error('PaymentForm, payment error:', result?.data?.error); + throw new Error( + result?.data?.error || + result?.serverError || + 'Failed to confirm payment' + ); + } + } else { + console.error('PaymentForm, no payment intent found'); + throw new Error("No payment intent found"); + } + } + } catch (error) { + console.error('PaymentForm, payment error:', error); + toast.error('Purchase credits failed'); + } finally { + setProcessing(false); + } + }; + + return ( +
+ + + +
+
+ +
+ {packageInfo.credits.toLocaleString()} +
+
+
+ {formatPrice(packageInfo.price, 'USD')} +
+
+
+
+ +
+

+ We use Stripe, a trusted global payment provider, to process your payment. + For your security, your payment details are handled directly by Stripe and never touch our servers. +

+
+
+
+ +
+ +
+ + +
+ +
+ ); +} diff --git a/src/config/sidebar-config.tsx b/src/config/sidebar-config.tsx index 2aa6936..2dd17cf 100644 --- a/src/config/sidebar-config.tsx +++ b/src/config/sidebar-config.tsx @@ -5,6 +5,7 @@ import type { NestedMenuItem } from '@/types'; import { BellIcon, CircleUserRoundIcon, + CoinsIcon, CreditCardIcon, LayoutDashboardIcon, LockKeyholeIcon, @@ -66,6 +67,12 @@ export function getSidebarLinks(): NestedMenuItem[] { href: Routes.SettingsBilling, external: false, }, + { + title: t('settings.credits.title'), + icon: , + href: Routes.SettingsCredits, + external: false, + }, { title: t('settings.security.title'), icon: , diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 07cc3f5..ba50ebb 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,12 +1,37 @@ export const PLACEHOLDER_IMAGE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAoJJREFUWEfFl4lu4zAMRO3cx/9/au6reMaOdkxTTl0grQFCRoqaT+SQotq2bV9N8rRt28xms87m83l553eZ/9vr9Wpkz+ezkT0ej+6dv1X81AFw7M4FBACPVn2c1Z3zLgDeJwHgeLFYdAARYioAEAKJEG2WAjl3gCwNYymQQ9b7/V4spmIAwO6Wy2VnAMikBWlDURBELf8CuN1uHQSrPwMAHK5WqwFELQ01AIXdAa7XawfAb3p6AOwK5+v1ugAoEq4FRSFLgavfQ49jAGQpAE5wjgGCeRrGdBArwHOPcwFcLpcGU1X0IsBuN5tNgYhaiFFwHTiAwq8I+O5xfj6fOz38K+X/fYAdb7fbAgFAjIJ6Aav3AYlQ6nfnDoDz0+lUxNiLALvf7XaDNGQ6GANQBKR85V27B4D3QQRw7hGIYlQKWGM79hSweyCUe1blXhEAogfABwHAXAcqSYkxCtHLUK3XBajSc4Dj8dilAeiSAgD2+30BAEKV4GKcAuDqB4TdYwBgPQByCgApUBoE4EJUGvxUjF3Q69/zLw3g/HA45ABKgdIQu+JPIyDnisCfAxAFNFM0EFNQ64gfS0EUoQP8ighrZSjn3oziZEQpauyKbfjbZchHUL/3AS/Dd30gAkxuRACgfO+EWQW8qwI1o+wseNuKcQiESjALvwNoMI0TcRzD4lFcPYwIM+JTF5x6HOs8yI7jeB5oKhpMRFH9UwaSCDB2Jmg4rc6E2TT0biIaG0rQhNqyhpHBcayTTSXH6vcDL7/sdqRK8LkwTsU499E8vRcAojHcZ4AxABdilgrp4lsXk8oVqgwh7+6H3phqd8J0Kk4vbx/+sZqCD/vNLya/5dT9fAH8g1WdNGgwbQAAAABJRU5ErkJggg=='; -// credit package definition (example) +// credit package definition (price in cents) export const CREDIT_PACKAGES = [ - { id: 'package-1', credits: 1000, price: 10 }, - { id: 'package-2', credits: 2500, price: 20 }, - { id: 'package-3', credits: 5000, price: 30 }, -]; + { + id: 'basic', + credits: 100, + price: 990, // 9.90 USD in cents + popular: false, + description: 'Perfect for getting started', + }, + { + id: 'standard', + credits: 200, + price: 1490, // 14.90 USD in cents + popular: true, + description: 'Most popular package', + }, + { + id: 'premium', + credits: 500, + price: 3990, // 39.90 USD in cents + popular: false, + description: 'Best value for heavy users', + }, + { + id: 'enterprise', + credits: 1000, + price: 6990, // 69.90 USD in cents + popular: false, + description: 'Tailored for enterprises', + }, +] as const; // free monthly credits (10% of the smallest package) export const FREE_MONTHLY_CREDITS = 50; diff --git a/src/payment/index.ts b/src/payment/index.ts index 0a3d09f..0d52125 100644 --- a/src/payment/index.ts +++ b/src/payment/index.ts @@ -2,8 +2,11 @@ import { websiteConfig } from '@/config/website'; import { StripeProvider } from './provider/stripe'; import type { CheckoutResult, + ConfirmPaymentIntentParams, CreateCheckoutParams, + CreatePaymentIntentParams, CreatePortalParams, + PaymentIntentResult, PaymentProvider, PortalResult, Subscription, @@ -92,3 +95,27 @@ export const getSubscriptions = async ( const provider = getPaymentProvider(); return provider.getSubscriptions(params); }; + +/** + * Create a payment intent + * @param params Parameters for creating the payment intent + * @returns Payment intent result + */ +export const createPaymentIntent = async ( + params: CreatePaymentIntentParams +): Promise => { + const provider = getPaymentProvider(); + return provider.createPaymentIntent(params); +}; + +/** + * Confirm a payment intent + * @param params Parameters for confirming the payment intent + * @returns True if successful + */ +export const confirmPaymentIntent = async ( + params: ConfirmPaymentIntentParams +): Promise => { + const provider = getPaymentProvider(); + return provider.confirmPaymentIntent(params); +}; diff --git a/src/payment/provider/stripe.ts b/src/payment/provider/stripe.ts index 44e2e66..1981476 100644 --- a/src/payment/provider/stripe.ts +++ b/src/payment/provider/stripe.ts @@ -7,12 +7,17 @@ import { findPriceInPlan, } from '@/lib/price-plan'; import { sendNotification } from '@/notification/notification'; +import { addCredits } from '@/lib/credits'; +import { CREDIT_TRANSACTION_TYPE } from '@/lib/constants'; import { desc, eq } from 'drizzle-orm'; import { Stripe } from 'stripe'; import { type CheckoutResult, + type ConfirmPaymentIntentParams, type CreateCheckoutParams, + type CreatePaymentIntentParams, type CreatePortalParams, + type PaymentIntentResult, type PaymentProvider, type PaymentStatus, PaymentTypes, @@ -397,6 +402,12 @@ export class StripeProvider implements PaymentProvider { await this.onOnetimePayment(session); } } + } else if (eventType.startsWith('payment_intent.')) { + // Handle payment intent events + if (eventType === 'payment_intent.succeeded') { + const paymentIntent = event.data.object as Stripe.PaymentIntent; + await this.onPaymentIntentSucceeded(paymentIntent); + } } } catch (error) { console.error('handle webhook event error:', error); @@ -632,6 +643,97 @@ export class StripeProvider implements PaymentProvider { await sendNotification(session.id, customerId, userId, amount); } + /** + * Handle payment intent succeeded event + * @param paymentIntent Stripe payment intent + */ + private async onPaymentIntentSucceeded( + paymentIntent: Stripe.PaymentIntent + ): Promise { + console.log(`>> Handle payment intent succeeded: ${paymentIntent.id}`); + + // Get metadata from payment intent + const { packageId, userId, credits } = paymentIntent.metadata; + + if (!packageId || !userId || !credits) { + console.warn( + `<< Missing metadata for payment intent ${paymentIntent.id}: packageId=${packageId}, userId=${userId}, credits=${credits}` + ); + return; + } + + try { + // Add credits to user account using existing addCredits method + await addCredits({ + userId, + amount: parseInt(credits), + type: CREDIT_TRANSACTION_TYPE.PURCHASE, + description: `Credit package purchase: ${packageId} - ${credits} credits for $${paymentIntent.amount / 100}`, + paymentId: paymentIntent.id, + }); + + console.log( + `<< Successfully processed payment intent ${paymentIntent.id}: Added ${credits} credits to user ${userId}` + ); + } catch (error) { + console.error( + `<< Error processing payment intent ${paymentIntent.id}:`, + error + ); + throw error; + } + } + + /** + * Create a payment intent + * @param params Parameters for creating the payment intent + * @returns Payment intent result + */ + public async createPaymentIntent( + params: CreatePaymentIntentParams + ): Promise { + const { amount, currency, metadata } = params; + + try { + const paymentIntent = await this.stripe.paymentIntents.create({ + amount, + currency, + metadata, + automatic_payment_methods: { + enabled: true, + }, + }); + + return { + id: paymentIntent.id, + clientSecret: paymentIntent.client_secret!, + }; + } catch (error) { + console.error('Create payment intent error:', error); + throw new Error('Failed to create payment intent'); + } + } + + /** + * Confirm a payment intent + * @param params Parameters for confirming the payment intent + * @returns True if successful + */ + public async confirmPaymentIntent( + params: ConfirmPaymentIntentParams + ): Promise { + const { paymentIntentId } = params; + + try { + const paymentIntent = await this.stripe.paymentIntents.retrieve(paymentIntentId); + + return paymentIntent.status === 'succeeded'; + } catch (error) { + console.error('Confirm payment intent error:', error); + throw new Error('Failed to confirm payment intent'); + } + } + /** * Map Stripe subscription interval to our own interval types * @param subscription Stripe subscription diff --git a/src/payment/types.ts b/src/payment/types.ts index 7b5bd00..7b80936 100644 --- a/src/payment/types.ts +++ b/src/payment/types.ts @@ -159,6 +159,30 @@ export interface getSubscriptionsParams { userId: string; } +/** + * Parameters for creating a payment intent + */ +export interface CreatePaymentIntentParams { + amount: number; + currency: string; + metadata?: Record; +} + +/** + * Result of creating a payment intent + */ +export interface PaymentIntentResult { + id: string; + clientSecret: string; +} + +/** + * Parameters for confirming a payment intent + */ +export interface ConfirmPaymentIntentParams { + paymentIntentId: string; +} + /** * Payment provider interface */ @@ -178,6 +202,16 @@ export interface PaymentProvider { */ getSubscriptions(params: getSubscriptionsParams): Promise; + /** + * Create a payment intent + */ + createPaymentIntent(params: CreatePaymentIntentParams): Promise; + + /** + * Confirm a payment intent + */ + confirmPaymentIntent(params: ConfirmPaymentIntentParams): Promise; + /** * Handle webhook events */ diff --git a/src/routes.ts b/src/routes.ts index 1f1adf2..cab64b1 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -33,6 +33,7 @@ export enum Routes { AdminUsers = '/admin/users', SettingsProfile = '/settings/profile', SettingsBilling = '/settings/billing', + SettingsCredits = '/settings/credits', SettingsSecurity = '/settings/security', SettingsNotifications = '/settings/notifications', @@ -76,6 +77,7 @@ export const protectedRoutes = [ Routes.AdminUsers, Routes.SettingsProfile, Routes.SettingsBilling, + Routes.SettingsCredits, Routes.SettingsSecurity, Routes.SettingsNotifications, ];