Skip to content

Commit 8a136dd

Browse files
committed
feat: Allow to pick currency when starting subscription
1 parent cc9b0f8 commit 8a136dd

2 files changed

Lines changed: 67 additions & 6 deletions

File tree

apps/api/src/controllers/Users.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ export class Users {
190190
public async createCheckoutSession(req: Request, res: Response, _next: NextFunction) {
191191
const auth = res.locals.auth as AuthResponse;
192192
const {id} = UtilitySchemas.id.parse(req.params);
193+
const {currency} = req.query;
193194

194195
// Check if billing is enabled
195196
if (!STRIPE_ENABLED || !stripe) {
@@ -245,6 +246,17 @@ export class Users {
245246
const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1);
246247
const billingCycleAnchor = Math.floor(nextMonth.getTime() / 1000);
247248

249+
// Validate currency if provided
250+
let checkoutCurrency: string | undefined;
251+
if (currency && typeof currency === 'string') {
252+
const validCurrencies = ['usd', 'eur', 'gbp'];
253+
if (validCurrencies.includes(currency.toLowerCase())) {
254+
checkoutCurrency = currency.toLowerCase();
255+
} else {
256+
return res.status(400).json({error: 'Invalid currency. Supported: USD, EUR, GBP'});
257+
}
258+
}
259+
248260
// Create checkout session
249261
// Note: proration_behavior cannot be set when one-time prices are included
250262
// The billing_cycle_anchor alone ensures the subscription is anchored to the 1st of the month
@@ -253,6 +265,7 @@ export class Users {
253265
customer: project.customer ?? undefined, // Use existing customer if available
254266
client_reference_id: project.id, // Store project ID for webhook
255267
line_items: lineItems,
268+
...(checkoutCurrency && {currency: checkoutCurrency}),
256269
subscription_data: {
257270
billing_cycle_anchor: billingCycleAnchor,
258271
},

apps/web/src/pages/settings/index.tsx

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
Input,
2828
Select,
2929
SelectContent,
30+
SelectItem,
3031
SelectItemWithDescription,
3132
SelectTrigger,
3233
SelectValue,
@@ -105,6 +106,8 @@ export default function Settings() {
105106
const [deleteConfirmText, setDeleteConfirmText] = useState('');
106107
const [resetConfirmText, setResetConfirmText] = useState('');
107108
const [isLoadingBilling, setIsLoadingBilling] = useState(false);
109+
const [selectedCurrency, setSelectedCurrency] = useState<string>('auto');
110+
const [showCurrencySelector, setShowCurrencySelector] = useState(false);
108111

109112
// Fetch current user's membership for the active project
110113
const {data: membershipData} = useSWR<{
@@ -252,7 +255,7 @@ export default function Settings() {
252255
setShowRegenerateDialog(true);
253256
};
254257

255-
const handleStartSubscription = async () => {
258+
const handleStartSubscription = async (currency: string = 'auto') => {
256259
if (!activeProject) return;
257260
if (!billingEnabled) {
258261
setErrorMessage('Billing is disabled on this instance.');
@@ -263,7 +266,13 @@ export default function Settings() {
263266
setIsLoadingBilling(true);
264267
setErrorMessage(null);
265268

266-
const response = await network.fetch<{url: string}>('POST', `/users/@me/projects/${activeProject.id}/checkout`);
269+
// Build URL with optional currency parameter
270+
const url =
271+
currency === 'auto'
272+
? `/users/@me/projects/${activeProject.id}/checkout`
273+
: `/users/@me/projects/${activeProject.id}/checkout?currency=${currency}`;
274+
275+
const response = await network.fetch<{url: string}>('POST', url);
267276

268277
// Redirect to Stripe checkout
269278
if (response.url) {
@@ -666,10 +675,49 @@ export default function Settings() {
666675
</p>
667676
</div>
668677

669-
<div className="flex justify-start">
670-
<Button onClick={handleStartSubscription} disabled={isLoadingBilling}>
671-
{isLoadingBilling ? 'Loading...' : 'Start Subscription'}
672-
</Button>
678+
<div className="flex flex-col gap-2">
679+
<div className="flex justify-start">
680+
<Button onClick={() => handleStartSubscription(selectedCurrency)} disabled={isLoadingBilling}>
681+
{isLoadingBilling ? 'Loading...' : 'Start Subscription'}
682+
</Button>
683+
</div>
684+
685+
<div className="flex flex-col gap-2">
686+
<button
687+
type="button"
688+
onClick={() => setShowCurrencySelector(!showCurrencySelector)}
689+
className="text-xs text-neutral-500 hover:text-neutral-700 transition-colors w-fit"
690+
>
691+
{showCurrencySelector ? 'Hide currency options' : 'Select a different currency'}
692+
</button>
693+
694+
<AnimatePresence>
695+
{showCurrencySelector && (
696+
<motion.div
697+
initial={{opacity: 0, height: 0}}
698+
animate={{opacity: 1, height: 'auto'}}
699+
exit={{opacity: 0, height: 0}}
700+
transition={{duration: 0.2}}
701+
className="overflow-hidden"
702+
>
703+
<div className="flex items-center gap-3 pt-1">
704+
<label className="text-sm font-medium text-neutral-600">Currency:</label>
705+
<Select value={selectedCurrency} onValueChange={setSelectedCurrency}>
706+
<SelectTrigger className="w-[200px]">
707+
<SelectValue />
708+
</SelectTrigger>
709+
<SelectContent>
710+
<SelectItem value="auto">Auto-detect</SelectItem>
711+
<SelectItem value="usd">USD ($) - US Dollar</SelectItem>
712+
<SelectItem value="eur">EUR (€) - Euro</SelectItem>
713+
<SelectItem value="gbp">GBP (£) - British Pound</SelectItem>
714+
</SelectContent>
715+
</Select>
716+
</div>
717+
</motion.div>
718+
)}
719+
</AnimatePresence>
720+
</div>
673721
</div>
674722
</div>
675723
)}

0 commit comments

Comments
 (0)