@venlyfinance/react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md ADDED
@@ -0,0 +1,23 @@
1
+ # Composition rules for coding agents
2
+
3
+ You are building a financial product UI on `@venlyfinance/react`. These rules exist because the failure modes of money UIs are specific; follow them over generic dashboard instincts.
4
+
5
+ ## Non-negotiable
6
+
7
+ 1. **Never hand-roll API calls, auth, retries, or transfer state.** Every read is a hook (`useAccounts`, `useTransfers`, `useRampRequests`, …); every regulated lifecycle is a flow machine (`useStagedTransfer`, `useFourEyesApproval`, `useRampLifecycle`). If you are writing `fetch` or a `useEffect` polling loop, stop – the hook exists.
8
+ 2. **Wrap the tree once** in `<VenlyProvider environment="mock">`. Mock mode needs zero credentials and zero network; it is the correct default for any demo, test, or first build. Going live is a constructor change, not a rewrite.
9
+ 3. **Never place `clientSecret` in browser code.** The provider throws if you try. For browser apps use `proxyClientOptions()` against your own backend route.
10
+ 4. **Money movement is stage-then-confirm.** Render a review step showing `state.staged` (the exact request) before calling `confirm()`. Never wire a form submit directly to execution.
11
+ 5. **Approval UIs render the rule, not the error.** Use `capability` from `useFourEyesApproval`: when `reason` is `"actor-is-creator"`, say that a second person must approve – do not show buttons that will be refused. On failure `"stale-version"`, refetch and let the operator re-decide; never auto-retry an approval.
12
+
13
+ ## Rendering money states
14
+
15
+ - Use `descriptor` from `useRampLifecycle` for status pills and timelines: `intent` gives the semantic colour, but always pair colour with a glyph or label – state is never carried by colour alone.
16
+ - A waiting state must answer: must I act, who is it waiting on, what still works. `descriptor.waitingOn` and `descriptor.explanation` carry this; render them instead of a bare "Pending".
17
+ - Amounts: tabular figures, currency code after the amount, and never render a debit in red as the only signal. Empty numeric cells are an em dash, not 0.
18
+ - Terminal failure states show the reason from the record; the status field is the explanation field.
19
+
20
+ ## Demo choreography (mock mode)
21
+
22
+ `useVenlyMock()` exposes the store controls. A credible end-to-end demo:
23
+ create party → `advanceVerification(id)` → create account → virtual bank account (note its `referenceCode`) → stage + confirm a transfer → `advanceTransfer(id)` → show the ledger. Inject failures with `failNext("CONFLICT")` to show the stale-version approval path – error states are part of the product.
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ Initial release.
6
+
7
+ - `<VenlyProvider>` for mock / staging / production, with a browser guard that refuses to construct credentialed clients in a bundle.
8
+ - Read hooks for parties, accounts, wallets, virtual bank accounts, transfers, ramp requests, reference data, and fee quotes; write hooks for the create operations; `venlyKeys` / `venlyQueries` factories underneath.
9
+ - Flow machines: `useStagedTransfer` (stage-then-confirm with the idempotency key pinned at staging), `useFourEyesApproval` (optimistic-locking version carried through, 409 → `"stale-version"`), `useRampLifecycle` (status descriptors answering must-I-act / waiting-on / what-still-works).
10
+ - `proxyClientOptions()` for the browser-safe production shape; its placeholder credential is the exported `VENLY_PROXY_SECRET_SENTINEL`, which the browser guard recognises as not-a-secret.
11
+ - The browser guard covers every path a secret can take into the provider: the top-level prop and both per-client options objects.
12
+ - 30 node:test cases against the SDK's mock transport; zero network.
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Venly NV
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OF THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # @venlyfinance/react
2
+
3
+ [![ci](https://github.com/Venly/venly-settlement-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/Venly/venly-settlement-sdk/actions/workflows/ci.yml)
4
+
5
+ Headless React layer for the [Venly Finance and Fundflow APIs](https://github.com/Venly/venly-settlement-sdk). Provider, TanStack Query hooks, and flow state machines for the lifecycles that make money movement different from CRUD: stage-then-confirm execution, four-eyes approval, and status models where "pending" has four different meanings.
6
+
7
+ No components, no CSS. This package owns data and state; your UI (or the one your coding agent assembles) owns the pixels. The full behaviour is covered by a node:test suite that runs with zero network.
8
+
9
+ ## Try it in 0 minutes (mock mode)
10
+
11
+ ```bash
12
+ npm install @venlyfinance/react @tanstack/react-query
13
+ ```
14
+
15
+ ```tsx
16
+ import { VenlyProvider, useAccounts } from "@venlyfinance/react";
17
+
18
+ function Accounts() {
19
+ const { data } = useAccounts();
20
+ return <ul>{data?.items.map((a) => <li key={a.id}>{a.name ?? a.id}</li>)}</ul>;
21
+ }
22
+
23
+ export default function App() {
24
+ return (
25
+ <VenlyProvider environment="mock">
26
+ <Accounts />
27
+ </VenlyProvider>
28
+ );
29
+ }
30
+ ```
31
+
32
+ Mock mode needs zero credentials and makes zero network calls: every hook answers from the SDK's stateful, spec-validated fixture store. Create a party, watch KYC advance, stage a transfer, approve a ramp request – the whole product works on your laptop before you have an API key.
33
+
34
+ ## What's in the box
35
+
36
+ | Export | What it does |
37
+ |---|---|
38
+ | `<VenlyProvider>` | Constructs the Finance + Fundflow clients for `mock`, `staging`, or `production`; brings its own QueryClient if the app has none |
39
+ | `useParties` `useAccounts` `useWallets` `useVirtualBankAccounts` `useTransfers` `useRampRequests` `useReferenceData` `useFeeQuote` … | Read hooks, one per API resource, cache keys managed for you |
40
+ | `useCreateParty` `useCreateAccount` `useCreateVirtualBankAccount` `useCreatePaymentSession` `useCreateRampRequest` | Write hooks with cache invalidation wired |
41
+ | `useStagedTransfer` | Stage-then-confirm machine: validate → freeze the exact request with a pinned idempotency key → execute once → poll to terminal |
42
+ | `useFourEyesApproval` | Approve/reject/cancel with the optimistic-locking `version` carried through; 409 surfaces as `"stale-version"` (refetch and re-decide, never auto-retry) |
43
+ | `useRampLifecycle` | One ramp request, polled until terminal, with a status descriptor answering: must I act, who is it waiting on, what still works |
44
+ | `venlyKeys` / `venlyQueries` | Query-key factory and pure `{queryKey, queryFn}` factories for prefetching, route loaders, and tests |
45
+ | `useVenlyMock` | The mock controls (call log, `failNext`, `advanceVerification`, `advanceTransfer`) – defined only in mock mode |
46
+ | `proxyClientOptions` | Browser-safe production wiring (below) |
47
+
48
+ ## The flow machines
49
+
50
+ The hooks above the line are conveniences. These are the point:
51
+
52
+ ```tsx
53
+ const t = useStagedTransfer();
54
+
55
+ // 1. Stage: validates and freezes the request. The idempotency key is pinned
56
+ // HERE, so a double-clicked confirm can only ever execute once.
57
+ t.stage({ kind: "fiat", senderAccountId, body: { currency: "EUR", amount: 250 } });
58
+
59
+ // 2. Your review screen renders t.state.staged – the exact request that will run.
60
+ // 3. Confirm: executes, then polls the transfer to COMPLETED or FAILED.
61
+ await t.confirm();
62
+ ```
63
+
64
+ ```tsx
65
+ const approval = useFourEyesApproval(request, currentUserId);
66
+
67
+ approval.capability; // { canApprove, canReject, canCancel, reason? }
68
+ // creators see canApprove: false – render the rule, not an error
69
+ await approval.approve(); // carries { version }; a 409 → failure "stale-version"
70
+ ```
71
+
72
+ ```tsx
73
+ const { descriptor } = useRampLifecycle(id);
74
+ descriptor.phase; // "action-required" | "waiting" | "in-flight" | "terminal"
75
+ descriptor.waitingOn; // "approver" | "counterparty-funds" | "venly" | null
76
+ descriptor.explanation; // one sentence a support ticket would otherwise ask
77
+ ```
78
+
79
+ ## Going live: never put credentials in the browser
80
+
81
+ The Venly APIs use OAuth2 client-credentials. A `clientSecret` in a browser bundle is full API access for anyone who opens devtools, so the provider **throws** if it sees one in a browser outside mock mode.
82
+
83
+ Two supported production shapes:
84
+
85
+ **Server-rendered surfaces** (RSC, route handlers): build the clients server-side with real credentials and pass them in via the `finance` / `fundflow` props.
86
+
87
+ **Browser apps**: the browser talks to your backend; your backend holds the credentials.
88
+
89
+ ```tsx
90
+ // client – no secrets anywhere in the bundle
91
+ const proxy = proxyClientOptions("/api/venly");
92
+ <VenlyProvider environment="production"
93
+ financeOptions={proxy.finance} fundflowOptions={proxy.fundflow} />
94
+ ```
95
+
96
+ ```ts
97
+ // server – e.g. a Next.js catch-all route handler at /api/venly/finance/[...path]
98
+ // Authenticate YOUR user first; then forward with the SDK's credentials.
99
+ import { VenlyFinanceClient } from "@venlyfinance/sdk";
100
+ const venly = new VenlyFinanceClient({
101
+ clientId: process.env.VENLY_CLIENT_ID!,
102
+ clientSecret: process.env.VENLY_CLIENT_SECRET!,
103
+ environment: "production",
104
+ });
105
+ export async function GET(req: Request, { params }: { params: { path: string[] } }) {
106
+ const result = await venly.request("GET", "/" + params.path.join("/"));
107
+ return Response.json(result);
108
+ }
109
+ ```
110
+
111
+ The proxy inherits your app's session security, not Venly's – enforce your own authentication and per-user authorization in the handler.
112
+
113
+ ## Design rules this package follows
114
+
115
+ - **Types derive from the OpenAPI-generated SDK types.** No hand-written API mirrors; a spec regeneration breaks this build instead of drifting silently.
116
+ - **TanStack retry is off by default.** The SDK already retries transient failures (429/5xx with backoff and `Retry-After`); a second retry layer multiplies latency and hides real errors.
117
+ - **Approvals never auto-retry.** A version conflict means the world changed; the operator re-decides against the new state.
118
+ - **Mock affordances cannot fire in production.** `useVenlyMock()` returns `undefined` handles outside mock mode.
119
+
120
+ ## Development
121
+
122
+ ```bash
123
+ npm install
124
+ npm run typecheck && npm run typecheck:test
125
+ npm test # node:test, zero network
126
+ npm run build
127
+ ```
128
+
129
+ MIT. Part of the [venly-settlement-sdk](https://github.com/Venly/venly-settlement-sdk) monorepo alongside `@venlyfinance/sdk` and `@venlyfinance/settlement-mcp`.
@@ -0,0 +1,71 @@
1
+ import { type RampRequest } from "@venlyfinance/sdk";
2
+ /**
3
+ * Client-side read of what the four-eyes rule allows right now. The API is
4
+ * the enforcer (creator ≠ approver, optimistic locking); this function only
5
+ * decides what to RENDER: an approve button that will certainly be refused
6
+ * is worse than an explanation of who can act.
7
+ */
8
+ export interface ApprovalCapability {
9
+ canApprove: boolean;
10
+ canReject: boolean;
11
+ canCancel: boolean;
12
+ /** Why approval is unavailable, when it is. */
13
+ reason?: "not-awaiting-approval" | "actor-is-creator";
14
+ }
15
+ /**
16
+ * Structural input: `createdBy` is carried by ramp-request LIST items but
17
+ * not by the detail DTO, so it is optional here and creator detection simply
18
+ * degrades (the API still enforces the rule) when it is absent.
19
+ */
20
+ export interface ApprovalSubject {
21
+ status?: RampRequest["status"];
22
+ createdBy?: string;
23
+ }
24
+ export declare function approvalCapabilities(request: ApprovalSubject | undefined, actorId?: string): ApprovalCapability;
25
+ /**
26
+ * Typed interpretation of an approval failure, so UI renders the correct
27
+ * next action instead of a generic error toast.
28
+ *
29
+ * - "stale-version": someone else acted first (HTTP 409, optimistic lock).
30
+ * Correct UI: refresh the request, re-render capabilities, let the
31
+ * operator re-decide against the NEW state. Never auto-retry an approval.
32
+ * - "forbidden": the API refused the actor (includes the server-enforced
33
+ * creator≠approver rule). Correct UI: show who can act.
34
+ */
35
+ export type ApprovalFailureKind = "stale-version" | "forbidden" | "not-found" | "validation" | "unknown";
36
+ export declare function interpretApprovalError(error: unknown): ApprovalFailureKind;
37
+ export type FourEyesState = {
38
+ phase: "idle";
39
+ } | {
40
+ phase: "submitting";
41
+ action: "approve" | "reject" | "cancel";
42
+ } | {
43
+ phase: "applied";
44
+ action: "approve" | "reject" | "cancel";
45
+ request: RampRequest;
46
+ } | {
47
+ phase: "failed";
48
+ action: "approve" | "reject" | "cancel";
49
+ failure: ApprovalFailureKind;
50
+ error: unknown;
51
+ };
52
+ /**
53
+ * Four-eyes decision flow for one ramp request. Carries the optimistic-
54
+ * locking `version` through every action; a 409 comes back as
55
+ * "stale-version" so the surface can refetch-and-re-decide.
56
+ *
57
+ * ```tsx
58
+ * const approval = useFourEyesApproval(request, currentUserEmail);
59
+ * if (approval.capability.canApprove) <Button onClick={approval.approve} />
60
+ * ```
61
+ */
62
+ export declare function useFourEyesApproval(request: (RampRequest & {
63
+ createdBy?: string;
64
+ }) | undefined, actorId?: string): {
65
+ state: FourEyesState;
66
+ capability: ApprovalCapability;
67
+ approve: () => Promise<void>;
68
+ reject: () => Promise<void>;
69
+ cancel: () => Promise<void>;
70
+ reset: () => void;
71
+ };
@@ -0,0 +1,91 @@
1
+ import { useState } from "react";
2
+ import { useQueryClient } from "@tanstack/react-query";
3
+ import { VenlyApiError } from "@venlyfinance/sdk";
4
+ import { useVenly } from "../provider.js";
5
+ import { venlyKeys } from "../keys.js";
6
+ export function approvalCapabilities(request, actorId) {
7
+ if (!request || request.status !== "AWAITING_APPROVAL") {
8
+ return {
9
+ canApprove: false,
10
+ canReject: false,
11
+ canCancel: request?.status === "AWAITING_FUNDS",
12
+ reason: "not-awaiting-approval",
13
+ };
14
+ }
15
+ if (actorId && request.createdBy && actorId === request.createdBy) {
16
+ // Four-eyes: the creator can cancel their own request but never approve
17
+ // or reject it. Rendering the buttons anyway teaches operators that
18
+ // errors are normal; hiding them teaches the control.
19
+ return { canApprove: false, canReject: false, canCancel: true, reason: "actor-is-creator" };
20
+ }
21
+ return { canApprove: true, canReject: true, canCancel: true };
22
+ }
23
+ export function interpretApprovalError(error) {
24
+ if (error instanceof VenlyApiError) {
25
+ if (error.status === 409)
26
+ return "stale-version";
27
+ if (error.status === 403)
28
+ return "forbidden";
29
+ if (error.status === 404)
30
+ return "not-found";
31
+ if (error.status === 400)
32
+ return "validation";
33
+ }
34
+ return "unknown";
35
+ }
36
+ /**
37
+ * Four-eyes decision flow for one ramp request. Carries the optimistic-
38
+ * locking `version` through every action; a 409 comes back as
39
+ * "stale-version" so the surface can refetch-and-re-decide.
40
+ *
41
+ * ```tsx
42
+ * const approval = useFourEyesApproval(request, currentUserEmail);
43
+ * if (approval.capability.canApprove) <Button onClick={approval.approve} />
44
+ * ```
45
+ */
46
+ export function useFourEyesApproval(request, actorId) {
47
+ const { fundflow } = useVenly();
48
+ const queryClient = useQueryClient();
49
+ const [state, setState] = useState({ phase: "idle" });
50
+ const act = async (action) => {
51
+ if (!request?.id)
52
+ return;
53
+ if (typeof request.version !== "number") {
54
+ setState({
55
+ phase: "failed",
56
+ action,
57
+ failure: "stale-version",
58
+ error: new Error("Ramp request carries no version; refetch it before acting so optimistic locking can protect the decision."),
59
+ });
60
+ return;
61
+ }
62
+ setState({ phase: "submitting", action });
63
+ try {
64
+ const body = { version: request.version };
65
+ const updated = action === "approve"
66
+ ? await fundflow.rampRequests.approve(request.id, body)
67
+ : action === "reject"
68
+ ? await fundflow.rampRequests.reject(request.id, body)
69
+ : await fundflow.rampRequests.cancel(request.id, body);
70
+ if (updated.id)
71
+ queryClient.setQueryData(venlyKeys.rampRequest(updated.id), updated);
72
+ void queryClient.invalidateQueries({ queryKey: ["venly", "ramp-requests"] });
73
+ setState({ phase: "applied", action, request: updated });
74
+ }
75
+ catch (error) {
76
+ setState({ phase: "failed", action, failure: interpretApprovalError(error), error });
77
+ // A stale version means the cached request lies; make every reader refetch.
78
+ if (interpretApprovalError(error) === "stale-version" && request.id) {
79
+ void queryClient.invalidateQueries({ queryKey: venlyKeys.rampRequest(request.id) });
80
+ }
81
+ }
82
+ };
83
+ return {
84
+ state,
85
+ capability: approvalCapabilities(request, actorId),
86
+ approve: () => act("approve"),
87
+ reject: () => act("reject"),
88
+ cancel: () => act("cancel"),
89
+ reset: () => setState({ phase: "idle" }),
90
+ };
91
+ }
@@ -0,0 +1,96 @@
1
+ import type { RampRequest } from "@venlyfinance/sdk";
2
+ export type RampStatus = NonNullable<RampRequest["status"]>;
3
+ /**
4
+ * A waiting state must answer four questions or it is an anxiety generator:
5
+ * how long, on which channel, must I act, and what still works. This
6
+ * descriptor encodes the answers per status so every surface renders the
7
+ * same truth. The status field is the explanation field.
8
+ */
9
+ export interface RampStatusDescriptor {
10
+ status: RampStatus | "UNKNOWN";
11
+ /** Coarse phase for layout decisions (queue grouping, timeline register). */
12
+ phase: "action-required" | "waiting" | "in-flight" | "terminal";
13
+ /** Who the request is waiting on; null when nobody (terminal states). */
14
+ waitingOn: "approver" | "counterparty-funds" | "venly" | null;
15
+ /** Terminal outcome, only when phase is "terminal". */
16
+ outcome?: "succeeded" | "failed" | "declined" | "cancelled";
17
+ /** Semantic intent for status pills. Rendered with a glyph AND a colour —
18
+ * state is never carried by colour alone. */
19
+ intent: "positive" | "negative" | "pending" | "neutral";
20
+ canApprove: boolean;
21
+ canCancel: boolean;
22
+ canEditAmount: boolean;
23
+ isTerminal: boolean;
24
+ /** Short human label, sentence case. */
25
+ label: string;
26
+ /** One sentence: what is happening and whether the reader must act. */
27
+ explanation: string;
28
+ }
29
+ export declare function describeRampStatus(status: RampStatus | undefined): RampStatusDescriptor;
30
+ export interface RampLifecycleOptions {
31
+ /** Poll interval while the request is non-terminal. Default 4000ms. */
32
+ pollIntervalMs?: number;
33
+ }
34
+ /**
35
+ * One ramp request, polled until terminal, with its status descriptor.
36
+ * Polling stops by itself the moment the status is terminal.
37
+ */
38
+ export declare function useRampLifecycle(id: string | undefined, options?: RampLifecycleOptions): {
39
+ request: NoInfer<{
40
+ id?: string;
41
+ companyId?: string;
42
+ companyName?: string;
43
+ rampType?: "ON_RAMP" | "OFF_RAMP";
44
+ status?: "AWAITING_APPROVAL" | "AWAITING_FUNDS" | "PROCESSING" | "SUCCEEDED" | "FAILED" | "BLOCKED" | "DENIED" | "REJECTED" | "CANCELLED";
45
+ amount?: number;
46
+ netAmount?: number;
47
+ fiatAmount?: number;
48
+ fiatNetAmount?: number;
49
+ cryptoAmount?: number;
50
+ fiatFeeAmount?: number;
51
+ exchangeRate?: number;
52
+ feePercentage?: number;
53
+ paymentReference?: string;
54
+ paymentReceived?: boolean;
55
+ blockchainTransactionHash?: string;
56
+ createdAt?: string;
57
+ companyBankAccount?: import("@venlyfinance/sdk").FundflowComponents["schemas"]["EurSepaCompanyBankAccountDto"] | import("@venlyfinance/sdk").FundflowComponents["schemas"]["GbpChapsCompanyBankAccountDto"] | import("@venlyfinance/sdk").FundflowComponents["schemas"]["GbpFpsCompanyBankAccountDto"] | import("@venlyfinance/sdk").FundflowComponents["schemas"]["OtherCurrencySwiftCompanyBankAccountDto"] | import("@venlyfinance/sdk").FundflowComponents["schemas"]["UsAchCompanyBankAccountDto"] | import("@venlyfinance/sdk").FundflowComponents["schemas"]["UsSwiftCompanyBankAccountDto"] | import("@venlyfinance/sdk").FundflowComponents["schemas"]["UsWireCompanyBankAccountDto"];
58
+ companyWallet?: import("@venlyfinance/sdk").FundflowComponents["schemas"]["CompanyWalletDto"];
59
+ depositBankAccount?: import("@venlyfinance/sdk").FundflowComponents["schemas"]["EurSepaDepositBankAccountDto"] | import("@venlyfinance/sdk").FundflowComponents["schemas"]["GbpChapsDepositBankAccountDto"] | import("@venlyfinance/sdk").FundflowComponents["schemas"]["GbpFpsDepositBankAccountDto"] | import("@venlyfinance/sdk").FundflowComponents["schemas"]["OtherCurrencySwiftDepositBankAccountDto"] | import("@venlyfinance/sdk").FundflowComponents["schemas"]["UsAchDepositBankAccountDto"] | import("@venlyfinance/sdk").FundflowComponents["schemas"]["UsSwiftDepositBankAccountDto"] | import("@venlyfinance/sdk").FundflowComponents["schemas"]["UsWireDepositBankAccountDto"];
60
+ depositWallet?: import("@venlyfinance/sdk").FundflowComponents["schemas"]["DepositWalletDto"];
61
+ fiatCurrency?: import("@venlyfinance/sdk").FundflowComponents["schemas"]["FiatCurrencyDto"];
62
+ cryptoCurrency?: import("@venlyfinance/sdk").FundflowComponents["schemas"]["CryptoCurrencyDto"];
63
+ events?: import("@venlyfinance/sdk").FundflowComponents["schemas"]["RampRequestEventDto"][];
64
+ version?: number;
65
+ amountReceived?: number;
66
+ }> | undefined;
67
+ descriptor: RampStatusDescriptor;
68
+ query: import("@tanstack/react-query").UseQueryResult<NoInfer<{
69
+ id?: string;
70
+ companyId?: string;
71
+ companyName?: string;
72
+ rampType?: "ON_RAMP" | "OFF_RAMP";
73
+ status?: "AWAITING_APPROVAL" | "AWAITING_FUNDS" | "PROCESSING" | "SUCCEEDED" | "FAILED" | "BLOCKED" | "DENIED" | "REJECTED" | "CANCELLED";
74
+ amount?: number;
75
+ netAmount?: number;
76
+ fiatAmount?: number;
77
+ fiatNetAmount?: number;
78
+ cryptoAmount?: number;
79
+ fiatFeeAmount?: number;
80
+ exchangeRate?: number;
81
+ feePercentage?: number;
82
+ paymentReference?: string;
83
+ paymentReceived?: boolean;
84
+ blockchainTransactionHash?: string;
85
+ createdAt?: string;
86
+ companyBankAccount?: import("@venlyfinance/sdk").FundflowComponents["schemas"]["EurSepaCompanyBankAccountDto"] | import("@venlyfinance/sdk").FundflowComponents["schemas"]["GbpChapsCompanyBankAccountDto"] | import("@venlyfinance/sdk").FundflowComponents["schemas"]["GbpFpsCompanyBankAccountDto"] | import("@venlyfinance/sdk").FundflowComponents["schemas"]["OtherCurrencySwiftCompanyBankAccountDto"] | import("@venlyfinance/sdk").FundflowComponents["schemas"]["UsAchCompanyBankAccountDto"] | import("@venlyfinance/sdk").FundflowComponents["schemas"]["UsSwiftCompanyBankAccountDto"] | import("@venlyfinance/sdk").FundflowComponents["schemas"]["UsWireCompanyBankAccountDto"];
87
+ companyWallet?: import("@venlyfinance/sdk").FundflowComponents["schemas"]["CompanyWalletDto"];
88
+ depositBankAccount?: import("@venlyfinance/sdk").FundflowComponents["schemas"]["EurSepaDepositBankAccountDto"] | import("@venlyfinance/sdk").FundflowComponents["schemas"]["GbpChapsDepositBankAccountDto"] | import("@venlyfinance/sdk").FundflowComponents["schemas"]["GbpFpsDepositBankAccountDto"] | import("@venlyfinance/sdk").FundflowComponents["schemas"]["OtherCurrencySwiftDepositBankAccountDto"] | import("@venlyfinance/sdk").FundflowComponents["schemas"]["UsAchDepositBankAccountDto"] | import("@venlyfinance/sdk").FundflowComponents["schemas"]["UsSwiftDepositBankAccountDto"] | import("@venlyfinance/sdk").FundflowComponents["schemas"]["UsWireDepositBankAccountDto"];
89
+ depositWallet?: import("@venlyfinance/sdk").FundflowComponents["schemas"]["DepositWalletDto"];
90
+ fiatCurrency?: import("@venlyfinance/sdk").FundflowComponents["schemas"]["FiatCurrencyDto"];
91
+ cryptoCurrency?: import("@venlyfinance/sdk").FundflowComponents["schemas"]["CryptoCurrencyDto"];
92
+ events?: import("@venlyfinance/sdk").FundflowComponents["schemas"]["RampRequestEventDto"][];
93
+ version?: number;
94
+ amountReceived?: number;
95
+ }>, Error>;
96
+ };
@@ -0,0 +1,148 @@
1
+ import { useQuery } from "@tanstack/react-query";
2
+ import { useVenly } from "../provider.js";
3
+ import { venlyQueries } from "../query-options.js";
4
+ const DESCRIPTORS = {
5
+ AWAITING_APPROVAL: {
6
+ phase: "action-required",
7
+ waitingOn: "approver",
8
+ intent: "pending",
9
+ canApprove: true,
10
+ canCancel: true,
11
+ canEditAmount: true,
12
+ isTerminal: false,
13
+ label: "Awaiting approval",
14
+ explanation: "A second person must approve before anything moves; the creator cannot approve their own request.",
15
+ },
16
+ AWAITING_FUNDS: {
17
+ phase: "waiting",
18
+ waitingOn: "counterparty-funds",
19
+ intent: "pending",
20
+ canApprove: false,
21
+ canCancel: true,
22
+ canEditAmount: false,
23
+ isTerminal: false,
24
+ label: "Awaiting funds",
25
+ explanation: "Approved and waiting for the incoming payment to arrive; nothing is blocked on you, and the request can still be cancelled.",
26
+ },
27
+ PROCESSING: {
28
+ phase: "in-flight",
29
+ waitingOn: "venly",
30
+ intent: "pending",
31
+ canApprove: false,
32
+ canCancel: false,
33
+ canEditAmount: false,
34
+ isTerminal: false,
35
+ label: "Processing",
36
+ explanation: "Funds are moving; no action is available until this settles.",
37
+ },
38
+ SUCCEEDED: {
39
+ phase: "terminal",
40
+ waitingOn: null,
41
+ outcome: "succeeded",
42
+ intent: "positive",
43
+ canApprove: false,
44
+ canCancel: false,
45
+ canEditAmount: false,
46
+ isTerminal: true,
47
+ label: "Succeeded",
48
+ explanation: "Settled; the transaction detail carries the final amounts and references.",
49
+ },
50
+ FAILED: {
51
+ phase: "terminal",
52
+ waitingOn: null,
53
+ outcome: "failed",
54
+ intent: "negative",
55
+ canApprove: false,
56
+ canCancel: false,
57
+ canEditAmount: false,
58
+ isTerminal: true,
59
+ label: "Failed",
60
+ explanation: "The movement failed; the request record carries the reason.",
61
+ },
62
+ BLOCKED: {
63
+ phase: "waiting",
64
+ waitingOn: "venly",
65
+ intent: "pending",
66
+ canApprove: false,
67
+ canCancel: false,
68
+ canEditAmount: false,
69
+ isTerminal: false,
70
+ label: "Blocked",
71
+ explanation: "Held for review on the Venly side; no action is available to you while the hold stands.",
72
+ },
73
+ DENIED: {
74
+ phase: "terminal",
75
+ waitingOn: null,
76
+ outcome: "declined",
77
+ intent: "negative",
78
+ canApprove: false,
79
+ canCancel: false,
80
+ canEditAmount: false,
81
+ isTerminal: true,
82
+ label: "Denied",
83
+ explanation: "Declined during review; create a new request if circumstances change.",
84
+ },
85
+ REJECTED: {
86
+ phase: "terminal",
87
+ waitingOn: null,
88
+ outcome: "declined",
89
+ intent: "negative",
90
+ canApprove: false,
91
+ canCancel: false,
92
+ canEditAmount: false,
93
+ isTerminal: true,
94
+ label: "Rejected",
95
+ explanation: "Rejected at the approval step; the reviewer's decision is final for this request.",
96
+ },
97
+ CANCELLED: {
98
+ phase: "terminal",
99
+ waitingOn: null,
100
+ outcome: "cancelled",
101
+ intent: "neutral",
102
+ canApprove: false,
103
+ canCancel: false,
104
+ canEditAmount: false,
105
+ isTerminal: true,
106
+ label: "Cancelled",
107
+ explanation: "Withdrawn before completion; no funds moved.",
108
+ },
109
+ };
110
+ export function describeRampStatus(status) {
111
+ if (!status || !(status in DESCRIPTORS)) {
112
+ return {
113
+ status: "UNKNOWN",
114
+ phase: "waiting",
115
+ waitingOn: null,
116
+ intent: "neutral",
117
+ canApprove: false,
118
+ canCancel: false,
119
+ canEditAmount: false,
120
+ isTerminal: false,
121
+ label: "Unknown status",
122
+ explanation: "The request carries a status this version does not recognise; refetch or upgrade.",
123
+ };
124
+ }
125
+ return { status, ...DESCRIPTORS[status] };
126
+ }
127
+ /**
128
+ * One ramp request, polled until terminal, with its status descriptor.
129
+ * Polling stops by itself the moment the status is terminal.
130
+ */
131
+ export function useRampLifecycle(id, options) {
132
+ const clients = useVenly();
133
+ const query = useQuery({
134
+ ...venlyQueries.rampRequest(clients, id ?? ""),
135
+ enabled: Boolean(id),
136
+ refetchInterval: (q) => {
137
+ const status = q.state.data?.status;
138
+ if (status && describeRampStatus(status).isTerminal)
139
+ return false;
140
+ return options?.pollIntervalMs ?? 4_000;
141
+ },
142
+ });
143
+ return {
144
+ request: query.data,
145
+ descriptor: describeRampStatus(query.data?.status),
146
+ query,
147
+ };
148
+ }