From 8a136dde55fd1fae1f2a2e285019beb35fc75977 Mon Sep 17 00:00:00 2001
From: Dries Augustyns
Date: Sun, 21 Dec 2025 11:53:32 +0100
Subject: [PATCH] feat: Allow to pick currency when starting subscription
---
apps/api/src/controllers/Users.ts | 13 ++++++
apps/web/src/pages/settings/index.tsx | 60 ++++++++++++++++++++++++---
2 files changed, 67 insertions(+), 6 deletions(-)
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 && (
+
+
+
+
+
+
+ )}
+
+
)}