Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions frontend/app/asset-owner/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -72,12 +72,17 @@ export default function AssetOwnerPage() {
const { isConnected, address } = useWallet();
const [plans, setPlans] = useState<Plan[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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);
Expand All @@ -103,6 +108,7 @@ export default function AssetOwnerPage() {
</div>

{/* Demo Mode Notice */}
{error && <div role="alert" className="rounded-2xl border border-red-500/20 bg-red-500/5 p-4 text-xs text-red-300">{error}</div>}
<div className="rounded-2xl border border-[#33C5E0]/20 bg-[#33C5E0]/5 p-4 flex items-center gap-3">
<span className="w-2 h-2 rounded-full bg-[#33C5E0] animate-pulse" />
<p className="text-xs text-[#33C5E0]">
Expand Down
26 changes: 16 additions & 10 deletions frontend/app/asset-owner/plans/[planId]/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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) {
Expand Down
19 changes: 12 additions & 7 deletions frontend/app/asset-owner/plans/[planId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
46 changes: 38 additions & 8 deletions frontend/app/asset-owner/plans/claim/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
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<Plan>(initialPlan);
Expand Down Expand Up @@ -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([]);
Expand Down
40 changes: 26 additions & 14 deletions frontend/app/asset-owner/plans/create/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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");
Expand Down
12 changes: 8 additions & 4 deletions frontend/app/asset-owner/plans/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<string | null>(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]);

Expand Down Expand Up @@ -211,6 +214,7 @@ export default function PlansPage() {
</div>

{/* Filters */}
{error && <div role="alert" className="rounded-xl border border-red-500/20 bg-red-500/5 p-4 text-xs text-red-300">{error}</div>}
<div className="flex flex-col sm:flex-row gap-3">
<div className="relative flex-1">
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" />
Expand Down
4 changes: 3 additions & 1 deletion frontend/app/lib/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
26 changes: 15 additions & 11 deletions frontend/app/lib/api/kyc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<KYCResponse> {
async getKYCStatus(walletAddress?: string): Promise<KYCResponse> {
const query = walletAddress
? `?wallet_address=${encodeURIComponent(walletAddress)}`
: "";
const response = await apiClient.get<ApiResponse<KYCResponse>>(
"/api/kyc/status"
`/api/kyc/status${query}`
);
return response.data!;
return response.data ?? (response as unknown as KYCResponse);
}

/**
Expand Down
9 changes: 6 additions & 3 deletions frontend/app/lib/api/plans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,13 @@ export class PlansAPI {
* Get a specific plan by ID
*/
async getPlan(planId: string): Promise<Plan> {
const response = await apiClient.get<ApiResponse<Plan>>(
const response = await apiClient.get<ApiResponse<Plan> | Plan>(
`/api/plans/${planId}`
);
return response.data!;
if (response && typeof response === "object" && "data" in response) {
return response.data!;
}
return response as Plan;
}

/**
Expand Down Expand Up @@ -242,7 +245,7 @@ export class PlansAPI {
*/
async getPlansByOwner(ownerAddress: string): Promise<Plan[]> {
const response = await apiClient.get<ApiResponse<Plan[]> | Plan[]>(
`/api/plans?owner=${ownerAddress}`
`/api/plans?owner=${encodeURIComponent(ownerAddress)}`
);
if (response && typeof response === "object" && "data" in response) {
return response.data as Plan[];
Expand Down
Loading
Loading