From 93d0b661aceeb2a07dea2d55a576dee768417ac0 Mon Sep 17 00:00:00 2001 From: Tundeakinbami-dev Date: Mon, 29 Jun 2026 16:26:56 +0100 Subject: [PATCH] Fix pnpm Node compatibility issue and update dependencies --- frontend/app/hooks/useInactivityTimer.ts | 124 +++--- frontend/app/lib/api/client.ts | 3 +- frontend/app/lib/api/plans.ts | 23 ++ frontend/components/WalletModal.tsx | 43 ++- frontend/components/dashboard/SidebarNav.tsx | 81 ++-- .../kyc/VerificationPendingView.tsx | 25 +- .../kyc/VerificationRejectedView.tsx | 20 +- frontend/components/kyc/steps/ReviewStep.tsx | 8 +- .../plans/EditInheritancePlanPanel.tsx | 26 +- .../plans/FiatAnchorDetailsForm.tsx | 356 ++++++++++++++++++ frontend/hooks/useInactivityTimer.ts | 302 +++++++++++++++ frontend/pnpm-lock.yaml | 73 ++-- frontend/pnpm-workspace.yaml | 14 + .../tests/components/ConnectButton.test.tsx | 4 +- .../EditInheritancePlanPanel.test.tsx | 2 +- .../tests/components/WalletModal.test.tsx | 2 +- .../tests/hooks/useInactivityTimer.test.ts | 132 +++---- frontend/tests/setup.ts | 20 +- 18 files changed, 1013 insertions(+), 245 deletions(-) create mode 100644 frontend/components/plans/FiatAnchorDetailsForm.tsx create mode 100644 frontend/hooks/useInactivityTimer.ts create mode 100644 frontend/pnpm-workspace.yaml diff --git a/frontend/app/hooks/useInactivityTimer.ts b/frontend/app/hooks/useInactivityTimer.ts index 61fccd961..7a1d3ece2 100644 --- a/frontend/app/hooks/useInactivityTimer.ts +++ b/frontend/app/hooks/useInactivityTimer.ts @@ -1,7 +1,7 @@ /** * Hook for managing inactivity countdown timer - * Provides client-side active timer based on blockchain last-ping */ + import { useState, useEffect, useCallback, useRef } from "react"; import { plansAPI } from "@/app/lib/api/plans"; @@ -12,7 +12,7 @@ export interface InactivityTimerState { seconds: number; lastPingTimestamp: number; isClaimable: boolean; - isSoonWarning: boolean; // True when <= 24 hours + isSoonWarning: boolean; } interface UseInactivityTimerOptions { @@ -22,39 +22,33 @@ interface UseInactivityTimerOptions { warningThresholdHours?: number; } -/** - * Calculate time remaining from timestamps - */ function calculateTimeRemaining( lastPingTimestamp: number, inactivityPeriodDays: number -): { - days: number; - hours: number; - minutes: number; - seconds: number; - totalSeconds: number; - isClaimable: boolean; -} { - const claimableAt = lastPingTimestamp + inactivityPeriodDays * 24 * 60 * 60 * 1000; - const now = Date.now(); - const remainingMs = Math.max(0, claimableAt - now); +) { + const claimableAt = + lastPingTimestamp + inactivityPeriodDays * 24 * 60 * 60 * 1000; + + const remainingMs = Math.max(0, claimableAt - Date.now()); const isClaimable = remainingMs === 0; const totalSeconds = Math.floor(remainingMs / 1000); - const days = Math.floor(totalSeconds / (24 * 60 * 60)); - const hours = Math.floor((totalSeconds % (24 * 60 * 60)) / (60 * 60)); - const minutes = Math.floor((totalSeconds % (60 * 60)) / 60); - const seconds = totalSeconds % 60; - return { days, hours, minutes, seconds, totalSeconds, isClaimable }; + return { + days: Math.floor(totalSeconds / (24 * 60 * 60)), + hours: Math.floor((totalSeconds % (24 * 60 * 60)) / (60 * 60)), + minutes: Math.floor((totalSeconds % (60 * 60)) / 60), + seconds: totalSeconds % 60, + totalSeconds, + isClaimable, + }; } export function useInactivityTimer(options: UseInactivityTimerOptions) { const { planId, enabled = true, - pollIntervalMs = 5000, // Poll every 5 seconds for status + pollIntervalMs = 5000, warningThresholdHours = 24, } = options; @@ -75,22 +69,21 @@ export function useInactivityTimer(options: UseInactivityTimerOptions) { const tickIntervalRef = useRef(null); const pollIntervalRef = useRef(null); - // Fetch initial inactivity status const fetchInactivityStatus = useCallback(async () => { if (!enabled) return; try { setLoading(true); + const status = await plansAPI.getInactivityStatus(planId); + setInactivityPeriodDays(status.inactivity_period_days); setError(null); - // Update timer state with fetched data const remaining = calculateTimeRemaining( status.last_ping_timestamp, status.inactivity_period_days ); - const isSoonWarning = remaining.totalSeconds < warningThresholdHours * 60 * 60; setTimerState({ days: remaining.days, @@ -99,40 +92,52 @@ export function useInactivityTimer(options: UseInactivityTimerOptions) { seconds: remaining.seconds, lastPingTimestamp: status.last_ping_timestamp, isClaimable: status.is_claimable || remaining.isClaimable, - isSoonWarning, + isSoonWarning: + remaining.totalSeconds < + warningThresholdHours * 60 * 60, }); } catch (err) { - setError(err instanceof Error ? err : new Error("Failed to fetch inactivity status")); + setError( + err instanceof Error + ? err + : new Error("Failed to fetch inactivity status") + ); } finally { setLoading(false); } }, [planId, enabled, warningThresholdHours]); - // Update timer every second (client-side tick) + // Initial fetch + polling (FIXED) useEffect(() => { - if (!enabled || !inactivityPeriodDays) return; + if (!enabled) return; + + const init = async () => { + await fetchInactivityStatus(); + }; - // Start with immediate fetch - fetchInactivityStatus(); + init(); - // Set up polling for fresh data from blockchain - pollIntervalRef.current = setInterval(fetchInactivityStatus, pollIntervalMs); + pollIntervalRef.current = setInterval(() => { + fetchInactivityStatus(); + }, pollIntervalMs); return () => { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); } }; - }, [enabled, inactivityPeriodDays, pollIntervalMs, fetchInactivityStatus]); + }, [enabled, pollIntervalMs, fetchInactivityStatus]); - // Client-side tick (every second) for smooth countdown + // Local countdown ticker useEffect(() => { - if (!enabled || !inactivityPeriodDays) return; + if (!enabled || inactivityPeriodDays === null) return; const tick = () => { setTimerState((prev) => { - const remaining = calculateTimeRemaining(prev.lastPingTimestamp, inactivityPeriodDays); - const isSoonWarning = remaining.totalSeconds < warningThresholdHours * 60 * 60; + const remaining = calculateTimeRemaining( + prev.lastPingTimestamp, + inactivityPeriodDays + ); return { days: remaining.days, @@ -141,7 +146,9 @@ export function useInactivityTimer(options: UseInactivityTimerOptions) { seconds: remaining.seconds, lastPingTimestamp: prev.lastPingTimestamp, isClaimable: remaining.isClaimable, - isSoonWarning, + isSoonWarning: + remaining.totalSeconds < + warningThresholdHours * 60 * 60, }; }); }; @@ -155,22 +162,27 @@ export function useInactivityTimer(options: UseInactivityTimerOptions) { }; }, [enabled, inactivityPeriodDays, warningThresholdHours]); - const ping = useCallback( - async (signedTransaction?: string) => { - try { - setError(null); - const updated = await plansAPI.pingKeepAlive(planId, signedTransaction); - // Refetch status to get updated timestamp - await fetchInactivityStatus(); - return updated; - } catch (err) { - const error = err instanceof Error ? err : new Error("Keep-alive ping failed"); - setError(error); - throw error; - } - }, - [planId, fetchInactivityStatus] - ); + const ping = useCallback(async (signedTransaction?: string) => { + try { + setError(null); + + const updated = await plansAPI.pingKeepAlive( + planId, + signedTransaction + ); + + await fetchInactivityStatus(); + return updated; + } catch (err) { + const error = + err instanceof Error + ? err + : new Error("Keep-alive ping failed"); + + setError(error); + throw error; + } + }, [planId, fetchInactivityStatus]); const refetch = useCallback(async () => { await fetchInactivityStatus(); @@ -183,4 +195,4 @@ export function useInactivityTimer(options: UseInactivityTimerOptions) { ping, refetch, }; -} +} \ No newline at end of file diff --git a/frontend/app/lib/api/client.ts b/frontend/app/lib/api/client.ts index 32ac95c49..fdbbe0050 100644 --- a/frontend/app/lib/api/client.ts +++ b/frontend/app/lib/api/client.ts @@ -14,7 +14,8 @@ const DEFAULT_RETRY_CONFIG: RetryConfig = { maxRetries: 3, baseDelayMs: 1000, maxDelayMs: 10_000, - retryOnStatuses: [408, 429, 500, 502, 503, 504], + // Only retry on transient network and gateway errors. + retryOnStatuses: [408, 429, 502, 503, 504], }; export interface ApiResponse { diff --git a/frontend/app/lib/api/plans.ts b/frontend/app/lib/api/plans.ts index 9c5e4daf6..e94371fce 100644 --- a/frontend/app/lib/api/plans.ts +++ b/frontend/app/lib/api/plans.ts @@ -27,11 +27,34 @@ export interface Plan { updated_at: string; } +export interface FiatAnchorInfo { + currency: string; + anchor_provider: string; + country: string; + bank_name?: string; + iban?: string; + routing_number?: string; + account_number?: string; + accept_fees: boolean; +} + +export interface FiatAnchorInfo { + currency: string; + anchor_provider: string; + country: string; + bank_name?: string; + iban?: string; + routing_number?: string; + account_number?: string; + accept_fees: boolean; +} + export interface Beneficiary { id?: string; wallet_address: string; name: string; allocation_percentage: number; + fiat_anchor_info?: string; } export interface CreatePlanRequest { diff --git a/frontend/components/WalletModal.tsx b/frontend/components/WalletModal.tsx index 9ceb04a9c..6899ed66a 100644 --- a/frontend/components/WalletModal.tsx +++ b/frontend/components/WalletModal.tsx @@ -2,7 +2,7 @@ import { useWallet } from "../context/WalletContext"; import { motion, AnimatePresence } from "framer-motion"; -import { Loader2, Wallet, Check } from "lucide-react"; +import { Check, Wallet } from "lucide-react"; import React from "react"; import UserIcon from "./userIcon"; @@ -10,10 +10,10 @@ export function WalletModal() { const { isModalOpen, closeModal, supportedWallets, connect, isConnecting } = useWallet(); - // If we wanted to "select" first before connecting, we'd need local state. + // If we wanted to "select" first before connecting, we would need local state. // But standard flow is click -> connect. - // However, the screenshot shows "Connect Wallet" button at the bottom. - // This implies: Select a wallet (radio) -> Click specific Connect button. + // However, the screenshot shows a "Connect Wallet" button at the bottom. + // This implies: Select a wallet (radio) -> Click the specific Connect button. // I will implement that flow. const [activeSelection, setActiveSelection] = React.useState( @@ -40,19 +40,21 @@ export function WalletModal() { className="fixed inset-0 z-40 bg-transparent" onClick={closeModal} /> -
+ +

Connect Wallet

+

Connect your wallet to get started with InheritX

@@ -61,16 +63,29 @@ export function WalletModal() {
{supportedWallets.map((wallet) => { const isSelected = activeSelection === wallet.id; + return ( -
+
+
@@ -114,4 +133,4 @@ export function WalletModal() { )} ); -} +} \ No newline at end of file diff --git a/frontend/components/dashboard/SidebarNav.tsx b/frontend/components/dashboard/SidebarNav.tsx index cfb8b7d78..c719fbc25 100644 --- a/frontend/components/dashboard/SidebarNav.tsx +++ b/frontend/components/dashboard/SidebarNav.tsx @@ -1,9 +1,11 @@ "use client"; + import Link from "next/link"; import { usePathname } from "next/navigation"; import { useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { X, Menu, ShieldCheck } from "lucide-react"; + import HomeIcon from "@/app/svg/HomeIcon"; import PlansIcon from "@/app/svg/PlansIcon"; import ClaimIcon from "@/app/svg/ClaimIcon"; @@ -42,100 +44,125 @@ const navItems = [ }, ]; -export function SidebarNav() { - const pathname = usePathname(); - const [isOpen, setIsOpen] = useState(false); - const NavLinks = () => ( -