diff --git a/README.md b/README.md index 23a82726..7109fd2c 100644 --- a/README.md +++ b/README.md @@ -893,6 +893,14 @@ function FundInvoiceButton() { The provider rehydrates from storage **after mount** (SSR-safe). `disconnect()` clears storage immediately. See [WALLET_INTEGRATION_CONTRACT.md](WALLET_INTEGRATION_CONTRACT.md) for the full integration contract. +**Funding-intent flow:** On the invoice detail page (`/invest/[id]`), the Fund button is wallet-gated: +- **Disconnected**: clicking the Fund button calls `connect()` to prompt wallet connection before funding. +- **Connecting / No wallet**: the button is disabled to prevent duplicate connection attempts. +- **Connected**: the Fund button is enabled and the partial-funding form (`FundAmountInput`) can be submitted. +- **Pending (in-flight)**: while a funding transaction is in progress, both the Fund button and the amount input are disabled and the button shows "Funding…" with `aria-busy="true"`. + +The wallet hook is consumed via `useWallet()` from `@/components/WalletProvider` (the canonical provider source), destructuring only `{ state, connect }` since `FundActions` does not need `walletData` or `disconnect`. + ### NavMenu `components/NavMenu.jsx` — Responsive site-wide header navigation used on every page. diff --git a/app/invest/[id]/FundActions.announce.test.tsx b/app/invest/[id]/FundActions.announce.test.tsx index d9e426f7..2811dd38 100644 --- a/app/invest/[id]/FundActions.announce.test.tsx +++ b/app/invest/[id]/FundActions.announce.test.tsx @@ -27,7 +27,7 @@ jest.mock("@/components/ToastProvider", () => ({ useToast: () => mockToast, })); -jest.mock("@/components/WalletContext", () => ({ +jest.mock("@/components/WalletProvider", () => ({ WALLET_STATES: { DISCONNECTED: "disconnected", CONNECTING: "connecting", @@ -48,7 +48,7 @@ jest.mock("@/app/invest/MarketplaceContext", () => ({ })); import FundActions from "./FundActions"; -import { useWallet, WALLET_STATES } from "@/components/WalletContext"; +import { useWallet, WALLET_STATES } from "@/components/WalletProvider"; import { copy } from "@/app/copy/en"; const mockUseWallet = useWallet as jest.MockedFunction; diff --git a/app/invest/[id]/FundActions.jsx b/app/invest/[id]/FundActions.jsx index 66125caa..31edc8f4 100644 --- a/app/invest/[id]/FundActions.jsx +++ b/app/invest/[id]/FundActions.jsx @@ -33,7 +33,9 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useToast } from "@/components/ToastProvider"; -import { useWallet, WALLET_STATES } from "@/components/WalletContext"; +// Canonical wallet hook — imports from the provider source of truth, +// not the deprecated WalletContext shim. +import { useWallet, WALLET_STATES } from "@/components/WalletProvider"; import FundAmountInput from "@/components/FundAmountInput"; import { useMarketplace } from "@/app/invest/MarketplaceContext"; import { copy } from "@/app/copy/en"; @@ -102,6 +104,10 @@ export async function copyInvoiceUrl(id) { * Defaults to a mock that resolves immediately (placeholder until Stellar lands). */ export default function FundActions({ id, status, maxAmount, currency, yieldValue, performFund }) { + // Wallet gating: destructure only the canonical shape + // { state, walletData, connect, disconnect } from the consolidated provider. + // `state` is aliased to `walletState` for clarity in the Fund button + // gating logic below. const { state: walletState, connect } = useWallet(); const toast = useToast(); const [isCopying, setIsCopying] = useState(false); diff --git a/app/invest/[id]/FundActions.optimistic.test.tsx b/app/invest/[id]/FundActions.optimistic.test.tsx index 507be68a..d94df845 100644 --- a/app/invest/[id]/FundActions.optimistic.test.tsx +++ b/app/invest/[id]/FundActions.optimistic.test.tsx @@ -26,7 +26,7 @@ jest.mock("@/components/ToastProvider", () => ({ useToast: () => mockToast, })); -jest.mock("@/components/WalletContext", () => ({ +jest.mock("@/components/WalletProvider", () => ({ WALLET_STATES: { DISCONNECTED: "disconnected", CONNECTING: "connecting", @@ -73,7 +73,7 @@ jest.mock("@/app/invest/MarketplaceContext", () => ({ useMarketplace: jest.fn(), })); -import { useWallet, WALLET_STATES } from "@/components/WalletContext"; +import { useWallet, WALLET_STATES } from "@/components/WalletProvider"; import { useMarketplace } from "@/app/invest/MarketplaceContext"; import FundActions from "./FundActions"; diff --git a/app/invest/[id]/InvoiceDetailClient.motion-contrast.test.tsx b/app/invest/[id]/InvoiceDetailClient.motion-contrast.test.tsx index 3bc30766..20d8b098 100644 --- a/app/invest/[id]/InvoiceDetailClient.motion-contrast.test.tsx +++ b/app/invest/[id]/InvoiceDetailClient.motion-contrast.test.tsx @@ -326,7 +326,7 @@ jest.mock("@/components/ToastProvider", () => ({ useToast: () => ({ success: jest.fn(), error: jest.fn(), info: jest.fn() }), })); -jest.mock("@/components/WalletContext", () => ({ +jest.mock("@/components/WalletProvider", () => ({ WALLET_STATES: { DISCONNECTED: "disconnected", CONNECTING: "connecting", diff --git a/app/invest/[id]/detail.a11y.test.tsx b/app/invest/[id]/detail.a11y.test.tsx index 06e3bd92..50520215 100644 --- a/app/invest/[id]/detail.a11y.test.tsx +++ b/app/invest/[id]/detail.a11y.test.tsx @@ -13,7 +13,7 @@ jest.mock("next/navigation", () => { }; }); -jest.mock("@/components/WalletContext", () => { +jest.mock("@/components/WalletProvider", () => { return { WALLET_STATES: { DISCONNECTED: "disconnected", diff --git a/app/invest/[id]/page.test.tsx b/app/invest/[id]/page.test.tsx index c9092b87..3cdc1237 100644 --- a/app/invest/[id]/page.test.tsx +++ b/app/invest/[id]/page.test.tsx @@ -52,7 +52,7 @@ jest.mock("@/components/ToastProvider", () => ({ useToast: () => mockToast, })); -jest.mock("@/components/WalletContext", () => ({ +jest.mock("@/components/WalletProvider", () => ({ WALLET_STATES: { DISCONNECTED: "disconnected", CONNECTING: "connecting", @@ -152,7 +152,7 @@ jest.mock("../lib", () => ({ import { notFound } from "next/navigation"; import { getInvoiceById } from "../lib"; -import { useWallet, WALLET_STATES } from "@/components/WalletContext"; +import { useWallet, WALLET_STATES } from "@/components/WalletProvider"; import { useOptimisticFund, FUNDING_STATES } from "@/lib/hooks/useOptimisticFund"; import InvoiceDetailPage from "./page"; import FundActions, { copyInvoiceUrl, copyToClipboardFallback } from "./FundActions";