diff --git a/frontend/app/asset-owner/page.tsx b/frontend/app/asset-owner/page.tsx index 07311f0bb..08f6120b3 100644 --- a/frontend/app/asset-owner/page.tsx +++ b/frontend/app/asset-owner/page.tsx @@ -4,8 +4,8 @@ import { useState, useEffect } from "react"; import Link from "next/link"; import { useWallet } from "@/context/WalletContext"; import { type Plan } from "@/app/lib/api/plans"; +import { getPlans } from "@/lib/api/dataSource"; import InactivityTimerCard from "@/components/plans/InactivityTimerCard"; -import { mockStore } from "@/lib/mockStore"; import { formatAddress } from "@/util/address"; import { TrendingUp, @@ -72,12 +72,17 @@ export default function AssetOwnerPage() { const { isConnected, address } = useWallet(); const [plans, setPlans] = useState([]); const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); useEffect(() => { - const mockPlans = mockStore.getPlans(); - setPlans(mockPlans); - setLoading(false); - }, [isConnected, address]); + let cancelled = false; + setLoading(true); + getPlans(address ?? undefined) + .then((nextPlans) => { if (!cancelled) { setPlans(nextPlans); setError(null); } }) + .catch((err) => { if (!cancelled) { setPlans([]); setError(err instanceof Error ? err.message : "Failed to load plans."); } }) + .finally(() => { if (!cancelled) setLoading(false); }); + return () => { cancelled = true; }; + }, [address]); const activePlans = plans.filter((p) => p.status?.toUpperCase() === "ACTIVE"); const totalYield = plans.reduce((s, p) => s + (p.accrued_yield ?? 0), 0); @@ -103,6 +108,7 @@ export default function AssetOwnerPage() { {/* Demo Mode Notice */} + {error &&
{error}
}

diff --git a/frontend/app/asset-owner/plans/[planId]/edit/page.tsx b/frontend/app/asset-owner/plans/[planId]/edit/page.tsx index b0a1782f5..9eb1898bd 100644 --- a/frontend/app/asset-owner/plans/[planId]/edit/page.tsx +++ b/frontend/app/asset-owner/plans/[planId]/edit/page.tsx @@ -4,6 +4,7 @@ import { useState, useEffect } from "react"; import { useRouter, useParams } from "next/navigation"; import { plansAPI } from "@/app/lib/api/plans"; import type { Plan } from "@/app/lib/api/plans"; +import { getPlan, useMockData } from "@/lib/api/dataSource"; import { EditInheritancePlanPanel } from "@/components/plans/EditInheritancePlanPanel"; import { Skeleton } from "@/components/ui/Skeleton"; import { AlertCircle } from "lucide-react"; @@ -19,21 +20,26 @@ export default function EditPlanPage() { useEffect(() => { if (!planId) return; - const mockPlan = require("@/lib/mockStore").mockStore.getPlan(planId); - if (mockPlan) { - setPlan(mockPlan); - } else { - setError("Plan not found."); - } - setLoading(false); + getPlan(planId) + .then((nextPlan) => { + if (nextPlan) setPlan(nextPlan); + else setError("Plan not found."); + }) + .catch((err) => setError(err instanceof Error ? err.message : "Failed to load plan.")) + .finally(() => setLoading(false)); }, [planId]); const handleClose = () => router.back(); const handleSaved = (updated: Plan) => { - require("@/lib/mockStore").mockStore.updatePlan(planId, updated); - setPlan(updated); - router.back(); + const save = useMockData + ? Promise.resolve(updated) + : plansAPI.updatePlan(planId, updated); + save.then((savedPlan) => { + if (useMockData) require("@/lib/mockStore").mockStore.updatePlan(planId, savedPlan); + setPlan(savedPlan); + router.back(); + }).catch((err) => setError(err instanceof Error ? err.message : "Failed to save plan.")); }; if (loading) { diff --git a/frontend/app/asset-owner/plans/[planId]/page.tsx b/frontend/app/asset-owner/plans/[planId]/page.tsx index b34d755ea..4b99eab53 100644 --- a/frontend/app/asset-owner/plans/[planId]/page.tsx +++ b/frontend/app/asset-owner/plans/[planId]/page.tsx @@ -4,6 +4,7 @@ import { useState, useEffect } from "react"; import { useParams, useRouter } from "next/navigation"; import Link from "next/link"; import { plansAPI, type Plan } from "@/app/lib/api/plans"; +import { getPlan } from "@/lib/api/dataSource"; import { ArrowLeft, FileText, @@ -73,13 +74,17 @@ export default function PlanDetailPage() { useEffect(() => { if (!planId) return; - const mockPlan = require("@/lib/mockStore").mockStore.getPlan(planId); - if (mockPlan) { - setPlan(mockPlan); - } else { - setError("Plan not found"); - } - setLoading(false); + let cancelled = false; + setLoading(true); + getPlan(planId) + .then((nextPlan) => { + if (cancelled) return; + if (nextPlan) setPlan(nextPlan); + else setError("Plan not found"); + }) + .catch((err) => { if (!cancelled) setError(err instanceof Error ? err.message : "Failed to load plan."); }) + .finally(() => { if (!cancelled) setLoading(false); }); + return () => { cancelled = true; }; }, [planId]); if (loading) { diff --git a/frontend/app/asset-owner/plans/claim/page.tsx b/frontend/app/asset-owner/plans/claim/page.tsx index 4852422bc..00ca9a08a 100644 --- a/frontend/app/asset-owner/plans/claim/page.tsx +++ b/frontend/app/asset-owner/plans/claim/page.tsx @@ -3,6 +3,8 @@ import { useState } from "react"; import { useWallet } from "@/context/WalletContext"; import { plansAPI, type Plan } from "@/app/lib/api/plans"; +import { getPlans, useMockData } from "@/lib/api/dataSource"; +import inheritanceAPI, { type PlanResponse } from "@/app/lib/api/inheritance"; import { useInactivityTimer } from "@/app/hooks/useInactivityTimer"; import { formatAddress } from "@/util/address"; import { motion, AnimatePresence } from "framer-motion"; @@ -26,6 +28,30 @@ interface PlanClaimCardProps { onSuccess: () => void; } +function toClaimPlan(plan: PlanResponse): Plan { + const amount = Number(plan.amount); + + return { + id: plan.id, + user_id: plan.owner_address, + owner_address: plan.owner_address, + token_address: plan.token_address, + amount, + title: "Inheritance Plan", + fee: 0, + net_amount: amount + plan.accrued_yield, + status: plan.status, + created_at: plan.created_at, + updated_at: plan.created_at, + contract_plan_id: undefined, + beneficiaries: plan.beneficiaries, + grace_period_seconds: plan.grace_period_seconds, + yield_rate_bps: plan.yield_rate_bps, + earn_yield: plan.earn_yield, + last_ping: plan.last_ping, + }; +} + function PlanClaimCard({ initialPlan, onSuccess }: PlanClaimCardProps) { const { kit, address: connectedAddress, isConnected, openModal } = useWallet(); const [currentPlan, setCurrentPlan] = useState(initialPlan); @@ -266,15 +292,19 @@ export default function ClaimPlanPage() { setSearched(true); try { - const mockStore = require("@/lib/mockStore").mockStore; - const query = searchQuery.trim().toLowerCase(); - // Search matching plan ID, owner address, or beneficiary address - const results = mockStore.getPlans().filter((p: any) => - p.id.toLowerCase().includes(query) || - p.owner_address.toLowerCase().includes(query) || - (p.beneficiaries && p.beneficiaries.some((b: any) => b.wallet_address.toLowerCase().includes(query))) + const query = searchQuery.trim(); + const results = useMockData + ? (await getPlans()).filter((p) => + p.id.toLowerCase().includes(query.toLowerCase()) || + p.owner_address?.toLowerCase().includes(query.toLowerCase()) || + p.beneficiaries?.some((b) => b.wallet_address?.toLowerCase().includes(query.toLowerCase())) + ) + : await inheritanceAPI.getPlans({ owner: query }); + setPlans( + (results || []).map((plan) => + "title" in plan ? plan : toClaimPlan(plan) + ) ); - setPlans(results || []); } catch (err) { setError(err instanceof Error ? err.message : "Failed to fetch plans for this address."); setPlans([]); diff --git a/frontend/app/asset-owner/plans/create/page.tsx b/frontend/app/asset-owner/plans/create/page.tsx index 22d4bf107..8e5b6f01e 100644 --- a/frontend/app/asset-owner/plans/create/page.tsx +++ b/frontend/app/asset-owner/plans/create/page.tsx @@ -13,6 +13,7 @@ import { Wallet, } from "lucide-react"; import inheritanceAPI from "@/app/lib/api/inheritance"; +import { useMockData } from "@/lib/api/dataSource"; import type { PlanBeneficiaryRequest } from "@/app/lib/api/inheritance"; import { getSelectedTokenIdentifier, @@ -175,20 +176,31 @@ export default function CreateInheritancePlanPage() { setSubmissionState("submitting"); try { - // Create local mock plan in mockStore - const newPlan = require("@/lib/mockStore").mockStore.createPlan({ - owner_address: hydratedDraft.owner, - token_address: selectedToken, - amount: Number(hydratedDraft.amount), - grace_period_seconds: (hydratedDraft.gracePeriodDays ?? 30) * 86400, - earn_yield: hydratedDraft.earnYield, - yield_rate_bps: 500, // 5% APY default - beneficiaries: buildBeneficiaries().map((b: any) => ({ - wallet_address: b.address, - allocation_bps: b.allocation_bps, - fiat_anchor_info: "anchor-ngn", - })), - }); + const newPlan = useMockData + ? require("@/lib/mockStore").mockStore.createPlan({ + owner_address: hydratedDraft.owner, + token_address: selectedToken, + amount: Number(hydratedDraft.amount), + grace_period_seconds: (hydratedDraft.gracePeriodDays ?? 30) * 86400, + earn_yield: hydratedDraft.earnYield, + yield_rate_bps: 500, + beneficiaries: buildBeneficiaries().map((b) => ({ + wallet_address: b.address, + allocation_bps: b.allocation_bps, + fiat_anchor_info: b.fiat_anchor_info, + })), + }) + : await inheritanceAPI.createPlan({ + owner: hydratedDraft.owner, + token: selectedToken, + amount: Number(hydratedDraft.amount), + beneficiaries: buildBeneficiaries(), + last_ping: Math.floor(Date.now() / 1000), + grace_period: (hydratedDraft.gracePeriodDays ?? 30) * 86400, + earn_yield: hydratedDraft.earnYield, + yield_rate_bps: 500, + is_active: true, + }); setCreatedPlanId(newPlan.id); setSubmissionState("success"); diff --git a/frontend/app/asset-owner/plans/page.tsx b/frontend/app/asset-owner/plans/page.tsx index 2a73190af..583b1caf4 100644 --- a/frontend/app/asset-owner/plans/page.tsx +++ b/frontend/app/asset-owner/plans/page.tsx @@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from "react"; import Link from "next/link"; import { useWallet } from "@/context/WalletContext"; import { plansAPI, type Plan } from "@/app/lib/api/plans"; +import { getPlans } from "@/lib/api/dataSource"; import { PlusCircle, Search, @@ -158,18 +159,20 @@ export default function PlansPage() { const [search, setSearch] = useState(""); const [statusFilter, setStatusFilter] = useState("All"); const [refreshing, setRefreshing] = useState(false); + const [error, setError] = useState(null); const fetchPlans = useCallback(async () => { try { - const mockPlans = require("@/lib/mockStore").mockStore.getPlans(); - setPlans(mockPlans); - } catch { + setPlans(await getPlans(address ?? undefined)); + setError(null); + } catch (err) { setPlans([]); + setError(err instanceof Error ? err.message : "Failed to load plans."); } finally { setLoading(false); setRefreshing(false); } - }, []); + }, [address]); useEffect(() => { fetchPlans(); }, [fetchPlans]); @@ -211,6 +214,7 @@ export default function PlansPage() {

{/* Filters */} + {error &&
{error}
}
diff --git a/frontend/app/lib/api/client.ts b/frontend/app/lib/api/client.ts index 32ac95c49..60ff237b0 100644 --- a/frontend/app/lib/api/client.ts +++ b/frontend/app/lib/api/client.ts @@ -128,7 +128,9 @@ export class ApiClient { const exponentialDelay = config.baseDelayMs * Math.pow(2, attempt); const cappedDelay = Math.min(exponentialDelay, config.maxDelayMs); // Full jitter: random value between 0 and cappedDelay - return Math.random() * cappedDelay; + const randomBytes = new Uint32Array(1); + globalThis.crypto?.getRandomValues(randomBytes); + return ((randomBytes[0] ?? 0) / 0xffffffff) * cappedDelay; } /** diff --git a/frontend/app/lib/api/kyc.ts b/frontend/app/lib/api/kyc.ts index 229418c75..ce7895b4d 100644 --- a/frontend/app/lib/api/kyc.ts +++ b/frontend/app/lib/api/kyc.ts @@ -32,29 +32,33 @@ export interface KYCResponse { } export interface KYCSubmissionRequest { - fullName: string; + wallet_address: string; + full_name: string; email: string; - dateOfBirth: string; + date_of_birth: string; nationality: string; - idType: string; - idNumber: string; - expiryDate: string; - streetAddress: string; + id_type: string; + id_number: string; + expiry_date: string; + street_address: string; city: string; country: string; - postalCode: string; - documentId?: string; // Reference to uploaded document + postal_code: string; + document_id?: string; } export class KycAPI { /** * Get current user's KYC status */ - async getKYCStatus(): Promise { + async getKYCStatus(walletAddress?: string): Promise { + const query = walletAddress + ? `?wallet_address=${encodeURIComponent(walletAddress)}` + : ""; const response = await apiClient.get>( - "/api/kyc/status" + `/api/kyc/status${query}` ); - return response.data!; + return response.data ?? (response as unknown as KYCResponse); } /** diff --git a/frontend/app/lib/api/plans.ts b/frontend/app/lib/api/plans.ts index b19982168..c7ea2582b 100644 --- a/frontend/app/lib/api/plans.ts +++ b/frontend/app/lib/api/plans.ts @@ -98,10 +98,13 @@ export class PlansAPI { * Get a specific plan by ID */ async getPlan(planId: string): Promise { - const response = await apiClient.get>( + const response = await apiClient.get | Plan>( `/api/plans/${planId}` ); - return response.data!; + if (response && typeof response === "object" && "data" in response) { + return response.data!; + } + return response as Plan; } /** @@ -242,7 +245,7 @@ export class PlansAPI { */ async getPlansByOwner(ownerAddress: string): Promise { const response = await apiClient.get | Plan[]>( - `/api/plans?owner=${ownerAddress}` + `/api/plans?owner=${encodeURIComponent(ownerAddress)}` ); if (response && typeof response === "object" && "data" in response) { return response.data as Plan[]; diff --git a/frontend/context/KYCContext.tsx b/frontend/context/KYCContext.tsx index fe18dcebc..5fe3149ee 100644 --- a/frontend/context/KYCContext.tsx +++ b/frontend/context/KYCContext.tsx @@ -2,6 +2,8 @@ import React, { createContext, useContext, useState, useEffect, useCallback } from "react"; import { kycAPI, type KYCStatus, type KYCResponse } from "@/app/lib/api/kyc"; +import { useWallet } from "./WalletContext"; +import { getKYCStatus, useMockData } from "@/lib/api/dataSource"; export type { KYCStatus }; @@ -74,6 +76,7 @@ export const useKYC = () => { }; export const KYCProvider = ({ children }: { children: React.ReactNode }) => { + const { address } = useWallet(); const [isKYCModalOpen, setIsKYCModalOpen] = useState(false); const [kycStatus, setKycStatus] = useState("pending"); const [kycResponse, setKycResponse] = useState(null); @@ -83,21 +86,22 @@ export const KYCProvider = ({ children }: { children: React.ReactNode }) => { const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - // Load persisted KYC status on mount and poll for updates + // Load status for the connected wallet and revalidate it while the app is open. useEffect(() => { const loadKYCStatus = async () => { try { setIsLoading(true); - const status = require("@/lib/mockStore").mockStore.getKYCStatus() as KYCStatus; - setKycStatus(status); - setKycResponse({ - wallet_address: "GDE2KZQ4QGJZ5Z5QW2Y4B7Y6Q5D3P9V8N7M6L5K4J3H2G1FTEST", - kyc_status: status, - submitted_at: new Date().toISOString(), - }); + if (!address && !useMockData) { + setKycResponse(null); + setKycStatus("pending"); + return; + } + const response = await getKYCStatus(address ?? undefined); + setKycResponse(response); + setKycStatus(response.kyc_status); setError(null); } catch (err) { - console.error("Failed to load KYC status:", err); + setError(err instanceof Error ? err.message : "Failed to load KYC status."); } finally { setIsLoading(false); } @@ -105,19 +109,10 @@ export const KYCProvider = ({ children }: { children: React.ReactNode }) => { loadKYCStatus(); - // Poll mockStore for updates - const pollInterval = setInterval(() => { - const status = require("@/lib/mockStore").mockStore.getKYCStatus() as KYCStatus; - setKycStatus(status); - setKycResponse({ - wallet_address: "GDE2KZQ4QGJZ5Z5QW2Y4B7Y6Q5D3P9V8N7M6L5K4J3H2G1FTEST", - kyc_status: status, - submitted_at: new Date().toISOString(), - }); - }, 5000); + const pollInterval = setInterval(loadKYCStatus, 5000); return () => clearInterval(pollInterval); - }, [kycStatus]); + }, [address]); const openKYCModal = () => setIsKYCModalOpen(true); const closeKYCModal = () => setIsKYCModalOpen(false); @@ -150,20 +145,31 @@ export const KYCProvider = ({ children }: { children: React.ReactNode }) => { setIsSubmitting(true); setError(null); try { - const mockStore = require("@/lib/mockStore").mockStore; - mockStore.setKYCStatus("pending"); - setKycStatus("pending"); - setKycResponse({ - wallet_address: "GDE2KZQ4QGJZ5Z5QW2Y4B7Y6Q5D3P9V8N7M6L5K4J3H2G1FTEST", - kyc_status: "pending", - submitted_at: new Date().toISOString(), - }); - - // Automatically approve after 8 seconds for nice user feedback - setTimeout(() => { - mockStore.setKYCStatus("approved"); - setKycStatus("approved"); - }, 8000); + if (useMockData) { + const mockStore = require("@/lib/mockStore").mockStore; + mockStore.setKYCStatus("pending"); + setKycStatus("pending"); + setTimeout(() => { + mockStore.setKYCStatus("approved"); + setKycStatus("approved"); + }, 8000); + } else { + await kycAPI.submitKYC({ + wallet_address: address ?? "", + full_name: formData.fullName, + email: formData.email, + date_of_birth: formData.dateOfBirth, + nationality: formData.nationality, + id_type: formData.idType, + id_number: formData.idNumber, + expiry_date: formData.expiryDate, + street_address: formData.streetAddress, + city: formData.city, + country: formData.country, + postal_code: formData.postalCode, + }); + await refreshKYCStatus(); + } setTimeout(() => { closeKYCModal(); @@ -181,7 +187,7 @@ export const KYCProvider = ({ children }: { children: React.ReactNode }) => { const refreshKYCStatus = useCallback(async () => { try { setIsLoading(true); - const response = await kycAPI.getKYCStatus(); + const response = await getKYCStatus(address ?? undefined); setKycResponse(response); setKycStatus(response.kyc_status); setError(null); @@ -192,7 +198,7 @@ export const KYCProvider = ({ children }: { children: React.ReactNode }) => { } finally { setIsLoading(false); } - }, []); + }, [address]); const canCreatePlan = kycStatus === "approved"; diff --git a/frontend/hooks/useApiQuery.ts b/frontend/hooks/useApiQuery.ts index 9a07ca0c4..1ce2843f8 100644 --- a/frontend/hooks/useApiQuery.ts +++ b/frontend/hooks/useApiQuery.ts @@ -10,6 +10,7 @@ interface UseApiQueryOptions { enabled?: boolean; onSuccess?: (data: T[]) => void; onError?: (error: Error) => void; + refreshInterval?: number; } interface ApiResponse { @@ -26,7 +27,7 @@ interface ApiResponse { } export function useApiQuery(options: UseApiQueryOptions) { - const { endpoint, initialParams, enabled = true, onSuccess, onError } = options; + const { endpoint, initialParams, enabled = true, onSuccess, onError, refreshInterval } = options; const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -60,15 +61,17 @@ export function useApiQuery(options: UseApiQueryOptions) { throw new Error(`HTTP error! status: ${response.status}`); } - const result: ApiResponse = await response.json(); + const result: ApiResponse | T[] = await response.json(); + const payload = Array.isArray(result) ? result : result.data; + if (!payload) throw new Error("API response did not contain data"); - setData(result.data); - if (result.pagination) { + setData(payload); + if (!Array.isArray(result) && result.pagination) { setPagination(result.pagination); } if (onSuccess) { - onSuccess(result.data); + onSuccess(payload); } } catch (err) { const error = err instanceof Error ? err : new Error("Unknown error"); @@ -83,7 +86,10 @@ export function useApiQuery(options: UseApiQueryOptions) { useEffect(() => { fetchData(); - }, [fetchData]); + if (!refreshInterval || refreshInterval <= 0) return; + const interval = window.setInterval(fetchData, refreshInterval); + return () => window.clearInterval(interval); + }, [fetchData, refreshInterval]); // Filter methods const setFilter = useCallback((key: string, value: any) => { diff --git a/frontend/lib/api/dataSource.ts b/frontend/lib/api/dataSource.ts new file mode 100644 index 000000000..04e145edd --- /dev/null +++ b/frontend/lib/api/dataSource.ts @@ -0,0 +1,34 @@ +import { plansAPI, type Plan } from "../../app/lib/api/plans"; +import { kycAPI, type KYCResponse } from "../../app/lib/api/kyc"; +import { mockStore } from "../mockStore"; + +export const useMockData = process.env.NEXT_PUBLIC_USE_MOCK_DATA === "true"; + +export async function getPlans(ownerAddress?: string): Promise { + if (useMockData) { + const plans = mockStore.getPlans(); + return ownerAddress ? plans.filter((plan) => plan.owner_address === ownerAddress) : plans; + } + + if (!ownerAddress) return []; + return plansAPI.getPlansByOwner(ownerAddress); +} + +export async function getPlan(planId: string): Promise { + if (useMockData) return mockStore.getPlan(planId); + + return plansAPI.getPlan(planId); +} + +export async function getKYCStatus(walletAddress?: string): Promise { + if (useMockData) { + return { + wallet_address: walletAddress ?? "demo-wallet", + kyc_status: mockStore.getKYCStatus() as KYCResponse["kyc_status"], + submitted_at: new Date().toISOString(), + }; + } + + if (!walletAddress) throw new Error("Connect a wallet to load KYC status."); + return kycAPI.getKYCStatus(walletAddress); +} \ No newline at end of file diff --git a/frontend/lib/mockStore.ts b/frontend/lib/mockStore.ts index c1dc75e65..414bfc937 100644 --- a/frontend/lib/mockStore.ts +++ b/frontend/lib/mockStore.ts @@ -111,7 +111,7 @@ export const mockStore = { createPlan(plan: Partial): MockPlan { const plans = this.getPlans(); const newPlan: MockPlan = { - id: `plan_${Math.random().toString(36).substr(2, 9)}`, + id: `plan_${globalThis.crypto.randomUUID()}`, user_id: "user_logged_in", owner_address: plan.owner_address || "GDRT7YV3XP9P...8K2M", token_address: plan.token_address || "USDC", diff --git a/frontend/lib/notifications/service.ts b/frontend/lib/notifications/service.ts index 9422ea02e..b762f4d08 100644 --- a/frontend/lib/notifications/service.ts +++ b/frontend/lib/notifications/service.ts @@ -219,7 +219,7 @@ class NotificationService { rendered: { subject?: string; title: string; body: string }, template: any ): Notification { - const id = `notif_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const id = `notif_${globalThis.crypto.randomUUID()}`; return { id,