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 */}
+ handlePurchase(pkg.id)}
+ disabled={loadingPackage === pkg.id}
+ className="w-full cursor-pointer"
+ variant={pkg.popular ? 'default' : 'outline'}
+ >
+ {loadingPackage === pkg.id ? (
+ <>
+
+ Processing...
+ >
+ ) : (
+ 'Purchase'
+ )}
+
+
+
+ ))}
+
+
+
+
+
+
+ {/* 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,
];