From 95400472e6a5adfcc461c41b993180a6698c23ba Mon Sep 17 00:00:00 2001 From: saj Date: Sun, 18 Oct 2026 18:54:10 +0100 Subject: [PATCH 1/4] fix: let users resend the password reset code with a cooldown (#528) Closes #528 Co-Authored-By: Claude Haiku 5.5 --- src/components/auth/ForgotPasswordForm.tsx | 28 +++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/components/auth/ForgotPasswordForm.tsx b/src/components/auth/ForgotPasswordForm.tsx index 1d69a9a..2f2a01d 100644 --- a/src/components/auth/ForgotPasswordForm.tsx +++ b/src/components/auth/ForgotPasswordForm.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import Link from "next/link"; import { ApiError } from "@/lib/api"; import { requestPasswordReset, resetPassword, MIN_ACCOUNT_PASSWORD_LENGTH } from "@/lib/passwordApi"; @@ -23,6 +23,13 @@ export function ForgotPasswordForm() { const [confirm, setConfirm] = useState(""); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); + const [cooldown, setCooldown] = useState(0); + + useEffect(() => { + if (cooldown <= 0) return; + const timer = setTimeout(() => setCooldown((s) => s - 1), 1000); + return () => clearTimeout(timer); + }, [cooldown]); async function onEmail(e: React.FormEvent) { e.preventDefault(); @@ -52,6 +59,17 @@ export function ForgotPasswordForm() { setStep("password"); } + // Sends a fresh code. The 30 s cooldown matches the signup resend button. + async function onResend() { + setError(null); + try { + await requestPasswordReset(email.trim()); + setCooldown(30); + } catch (err) { + setError(err instanceof Error ? err.message : "Could not resend the code."); + } + } + async function onPassword(e: React.FormEvent) { e.preventDefault(); if (password.length < MIN_ACCOUNT_PASSWORD_LENGTH) { @@ -135,6 +153,14 @@ export function ForgotPasswordForm() { If an account exists for that email, we sent a 6-digit code to it.

+ {errorBox} + + + )} + {step === "not-found" && (
From 2194f614c5d718460252379d9b7067fc37e667bd Mon Sep 17 00:00:00 2001 From: saj Date: Sun, 18 Oct 2026 18:54:14 +0100 Subject: [PATCH 3/4] fix: validate the flexible payment amount as the payer types (#530) Closes #530 Co-Authored-By: Claude Haiku 5.5 --- src/app/pay/[slug]/page.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/app/pay/[slug]/page.tsx b/src/app/pay/[slug]/page.tsx index 58968eb..634a7c6 100644 --- a/src/app/pay/[slug]/page.tsx +++ b/src/app/pay/[slug]/page.tsx @@ -426,7 +426,7 @@ export default function PayPage({ ? `$${formatStroops(link.amount_usdc_stroops)}` : step !== "form" && intent ? `$${formatStroops(intent.amount_usdc_stroops)}` - : amount + : amount && usdAmountToStroops(amount) !== null ? `$${amount}` : "$—"}

@@ -493,9 +493,16 @@ export default function PayPage({ id="payer-amount" value={amount} onChange={(e) => setAmount(e.target.value)} + inputMode="decimal" + aria-invalid={amount.trim() !== "" && usdAmountToStroops(amount) === null} placeholder="0.00" className="w-full rounded-lg border border-gray-300 px-3 py-2.5 text-sm text-gray-900 outline-none focus:border-black" /> + {amount.trim() !== "" && usdAmountToStroops(amount) === null && ( +

+ Enter an amount in USD, up to 7 decimal places. +

+ )} )} From b55d450dde934304bca075b84eb9e9f366c14e4d Mon Sep 17 00:00:00 2001 From: saj Date: Sun, 18 Oct 2026 18:54:15 +0100 Subject: [PATCH 4/4] fix: show progress and errors when refreshing wallet balances (#531) Closes #531 Co-Authored-By: Claude Haiku 5.5 --- src/app/dashboard/wallets/[id]/page.tsx | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/app/dashboard/wallets/[id]/page.tsx b/src/app/dashboard/wallets/[id]/page.tsx index db54d97..bc61fe2 100644 --- a/src/app/dashboard/wallets/[id]/page.tsx +++ b/src/app/dashboard/wallets/[id]/page.tsx @@ -62,13 +62,28 @@ export default function WalletOverview({ const [showTrustline, setShowTrustline] = useState(false); const [showRecover, setShowRecover] = useState(false); const [showChangePassword, setShowChangePassword] = useState(false); + const [refreshingBalances, setRefreshingBalances] = useState(false); function refresh() { if (!token) return; - getBalances(token, id).then(setBalances).catch(() => {}); + getBalances(token, id) + .then(setBalances) + .catch((e) => toast.error(errorMessage(e, "Could not refresh balances."))); listRecentTransactions(token, id).then(setTxns).catch(() => {}); } + async function refreshBalances() { + if (!token) return; + setRefreshingBalances(true); + try { + setBalances(await getBalances(token, id)); + } catch (e) { + toast.error(errorMessage(e, "Could not refresh balances.")); + } finally { + setRefreshingBalances(false); + } + } + useEffect(() => { if (!token) return; const controller = new AbortController(); @@ -230,8 +245,9 @@ export default function WalletOverview({ disabled={hasUsdc} /> token && getBalances(token, id).then(setBalances)} + label={refreshingBalances ? "Refreshing…" : "Refresh balances"} + onClick={refreshBalances} + disabled={refreshingBalances} /> setShowRecover(true)} /> setShowChangePassword(true)} />