diff --git a/apps/api/src/controllers/Users.ts b/apps/api/src/controllers/Users.ts index 99ca5f5..5c00d7f 100644 --- a/apps/api/src/controllers/Users.ts +++ b/apps/api/src/controllers/Users.ts @@ -190,6 +190,7 @@ export class Users { public async createCheckoutSession(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth as AuthResponse; const {id} = UtilitySchemas.id.parse(req.params); + const {currency} = req.query; // Check if billing is enabled if (!STRIPE_ENABLED || !stripe) { @@ -245,6 +246,17 @@ export class Users { const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1); const billingCycleAnchor = Math.floor(nextMonth.getTime() / 1000); + // Validate currency if provided + let checkoutCurrency: string | undefined; + if (currency && typeof currency === 'string') { + const validCurrencies = ['usd', 'eur', 'gbp']; + if (validCurrencies.includes(currency.toLowerCase())) { + checkoutCurrency = currency.toLowerCase(); + } else { + return res.status(400).json({error: 'Invalid currency. Supported: USD, EUR, GBP'}); + } + } + // Create checkout session // Note: proration_behavior cannot be set when one-time prices are included // The billing_cycle_anchor alone ensures the subscription is anchored to the 1st of the month @@ -253,6 +265,7 @@ export class Users { customer: project.customer ?? undefined, // Use existing customer if available client_reference_id: project.id, // Store project ID for webhook line_items: lineItems, + ...(checkoutCurrency && {currency: checkoutCurrency}), subscription_data: { billing_cycle_anchor: billingCycleAnchor, }, diff --git a/apps/web/src/pages/settings/index.tsx b/apps/web/src/pages/settings/index.tsx index e7fc62e..1d812bc 100644 --- a/apps/web/src/pages/settings/index.tsx +++ b/apps/web/src/pages/settings/index.tsx @@ -27,6 +27,7 @@ import { Input, Select, SelectContent, + SelectItem, SelectItemWithDescription, SelectTrigger, SelectValue, @@ -105,6 +106,8 @@ export default function Settings() { const [deleteConfirmText, setDeleteConfirmText] = useState(''); const [resetConfirmText, setResetConfirmText] = useState(''); const [isLoadingBilling, setIsLoadingBilling] = useState(false); + const [selectedCurrency, setSelectedCurrency] = useState('auto'); + const [showCurrencySelector, setShowCurrencySelector] = useState(false); // Fetch current user's membership for the active project const {data: membershipData} = useSWR<{ @@ -252,7 +255,7 @@ export default function Settings() { setShowRegenerateDialog(true); }; - const handleStartSubscription = async () => { + const handleStartSubscription = async (currency: string = 'auto') => { if (!activeProject) return; if (!billingEnabled) { setErrorMessage('Billing is disabled on this instance.'); @@ -263,7 +266,13 @@ export default function Settings() { setIsLoadingBilling(true); setErrorMessage(null); - const response = await network.fetch<{url: string}>('POST', `/users/@me/projects/${activeProject.id}/checkout`); + // Build URL with optional currency parameter + const url = + currency === 'auto' + ? `/users/@me/projects/${activeProject.id}/checkout` + : `/users/@me/projects/${activeProject.id}/checkout?currency=${currency}`; + + const response = await network.fetch<{url: string}>('POST', url); // Redirect to Stripe checkout if (response.url) { @@ -666,10 +675,49 @@ export default function Settings() {

-
- +
+
+ +
+ +
+ + + + {showCurrencySelector && ( + +
+ + +
+
+ )} +
+
)}