feat: Allow to pick currency when starting subscription

This commit is contained in:
Dries Augustyns
2025-12-21 11:53:32 +01:00
parent cc9b0f8a73
commit 8a136dde55
2 changed files with 67 additions and 6 deletions
+13
View File
@@ -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,
},
+54 -6
View File
@@ -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<string>('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() {
</p>
</div>
<div className="flex justify-start">
<Button onClick={handleStartSubscription} disabled={isLoadingBilling}>
{isLoadingBilling ? 'Loading...' : 'Start Subscription'}
</Button>
<div className="flex flex-col gap-2">
<div className="flex justify-start">
<Button onClick={() => handleStartSubscription(selectedCurrency)} disabled={isLoadingBilling}>
{isLoadingBilling ? 'Loading...' : 'Start Subscription'}
</Button>
</div>
<div className="flex flex-col gap-2">
<button
type="button"
onClick={() => setShowCurrencySelector(!showCurrencySelector)}
className="text-xs text-neutral-500 hover:text-neutral-700 transition-colors w-fit"
>
{showCurrencySelector ? 'Hide currency options' : 'Select a different currency'}
</button>
<AnimatePresence>
{showCurrencySelector && (
<motion.div
initial={{opacity: 0, height: 0}}
animate={{opacity: 1, height: 'auto'}}
exit={{opacity: 0, height: 0}}
transition={{duration: 0.2}}
className="overflow-hidden"
>
<div className="flex items-center gap-3 pt-1">
<label className="text-sm font-medium text-neutral-600">Currency:</label>
<Select value={selectedCurrency} onValueChange={setSelectedCurrency}>
<SelectTrigger className="w-[200px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto">Auto-detect</SelectItem>
<SelectItem value="usd">USD ($) - US Dollar</SelectItem>
<SelectItem value="eur">EUR () - Euro</SelectItem>
<SelectItem value="gbp">GBP (£) - British Pound</SelectItem>
</SelectContent>
</Select>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</div>
)}