From 3b9a6202e693e7b6d47a9265d71dcc7a9629aa2c Mon Sep 17 00:00:00 2001 From: martinzhames Date: Sun, 30 Aug 2026 08:57:09 +0100 Subject: [PATCH] feat: show remaining verification session time in modal SMEs mid-batch repay need to know when their verification session expires to avoid surprise re-verification prompts. Wires the verifiedAt/expiry tracked in walletStore into VerificationModal as a live countdown. --- components/wallet/VerificationModal.tsx | 35 +++++++++++++++++++++- components/wallet/VerificationProvider.tsx | 6 ++++ messages/ar.json | 1 + messages/en.json | 1 + messages/es.json | 1 + messages/pt-BR.json | 1 + store/walletStore.ts | 3 ++ 7 files changed, 47 insertions(+), 1 deletion(-) diff --git a/components/wallet/VerificationModal.tsx b/components/wallet/VerificationModal.tsx index a833c26b..2ec2dc89 100644 --- a/components/wallet/VerificationModal.tsx +++ b/components/wallet/VerificationModal.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { ShieldCheck, Loader2 } from "lucide-react"; import { useTranslations } from "next-intl"; import { @@ -19,21 +19,48 @@ interface VerificationModalProps { actionType?: string; /** The challenge message that will be signed — shown to the user for transparency */ challengeMessage?: string; + /** Epoch ms when the current verification session expires, if still active */ + sessionExpiresAt?: number; onVerify: () => Promise; onCancel: () => void; } +function formatRemaining(ms: number): string { + const totalSeconds = Math.max(0, Math.floor(ms / 1000)); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + return `${minutes}:${seconds.toString().padStart(2, "0")}`; +} + export function VerificationModal({ isOpen, isLoading = false, error, actionType, challengeMessage, + sessionExpiresAt, onVerify, onCancel, }: VerificationModalProps) { const t = useTranslations("verification"); const [localError, setLocalError] = useState(null); + const [remainingMs, setRemainingMs] = useState( + sessionExpiresAt ? sessionExpiresAt - Date.now() : null + ); + + useEffect(() => { + if (!sessionExpiresAt) { + setRemainingMs(null); + return; + } + + setRemainingMs(sessionExpiresAt - Date.now()); + const interval = setInterval(() => { + setRemainingMs(sessionExpiresAt - Date.now()); + }, 1000); + + return () => clearInterval(interval); + }, [sessionExpiresAt]); const handleVerify = async () => { try { @@ -64,6 +91,12 @@ export function VerificationModal({
+ {remainingMs !== null && remainingMs > 0 && ( +

+ {t("sessionExpiresIn", { time: formatRemaining(remainingMs) })} +

+ )} + {challengeMessage && (
Promise; @@ -136,6 +137,11 @@ export function VerificationProvider({ children }: { children: ReactNode }) { error={error ?? undefined} actionType={actionType} challengeMessage={challengeMessage} + sessionExpiresAt={ + wallet.isVerified && wallet.verifiedAt + ? wallet.verifiedAt + VERIFICATION_EXPIRY_MS + : undefined + } onVerify={handleVerify} onCancel={handleCancel} /> diff --git a/messages/ar.json b/messages/ar.json index 9e38ecae..f428033c 100644 --- a/messages/ar.json +++ b/messages/ar.json @@ -727,6 +727,7 @@ "descriptionDefault": "أثبت ملكيتك لهذه المحفظة بتوقيع رسالة. دون معاملات أو رسوم غاز.", "messageToSign": "الرسالة المراد توقيعها:", "challengeLabel": "رسالة التحدي المراد توقيعها", + "sessionExpiresIn": "تنتهي الجلسة خلال {time}", "signAndVerify": "توقيع والتحقق", "verifying": "جارٍ التحقق…", "cancel": "إلغاء" diff --git a/messages/en.json b/messages/en.json index 0139739c..fee49b53 100644 --- a/messages/en.json +++ b/messages/en.json @@ -698,6 +698,7 @@ "descriptionDefault": "Prove you own this wallet by signing a message. No transaction or gas fees.", "messageToSign": "Message to sign:", "challengeLabel": "Challenge message to be signed", + "sessionExpiresIn": "Session expires in {time}", "signAndVerify": "Sign & Verify", "verifying": "Verifying…", "cancel": "Cancel" diff --git a/messages/es.json b/messages/es.json index c8ce66eb..00345b6d 100644 --- a/messages/es.json +++ b/messages/es.json @@ -727,6 +727,7 @@ "descriptionDefault": "Prueba que eres dueño de esta billetera firmando un mensaje. Sin transacciones ni tarifas de gas.", "messageToSign": "Mensaje a firmar:", "challengeLabel": "Mensaje de desafío a firmar", + "sessionExpiresIn": "La sesión expira en {time}", "signAndVerify": "Firmar y Verificar", "verifying": "Verificando…", "cancel": "Cancelar" diff --git a/messages/pt-BR.json b/messages/pt-BR.json index 287b7b60..c0c29e30 100644 --- a/messages/pt-BR.json +++ b/messages/pt-BR.json @@ -727,6 +727,7 @@ "descriptionDefault": "Prove que você é dono desta carteira assinando uma mensagem. Sem transações ou taxas de gas.", "messageToSign": "Mensagem para assinar:", "challengeLabel": "Mensagem de desafio a ser assinada", + "sessionExpiresIn": "A sessão expira em {time}", "signAndVerify": "Assinar e Verificar", "verifying": "Verificando…", "cancel": "Cancelar" diff --git a/store/walletStore.ts b/store/walletStore.ts index 6cf46712..677662c3 100644 --- a/store/walletStore.ts +++ b/store/walletStore.ts @@ -15,6 +15,9 @@ const EMPTY_BALANCE: WalletBalance = { /** Session expires after 24 hours of inactivity. Change this constant to adjust. */ export const SESSION_EXPIRY_MS = 24 * 60 * 60 * 1000; +/** Wallet ownership verification expires 1 hour after it was granted. */ +export const VERIFICATION_EXPIRY_MS = 60 * 60 * 1000; + /** * Clears all address-scoped caches: TanStack Query, zustand persisted data, * and IndexedDB marketplace cache. Call on disconnect and session expiry.