@gatedpath/frontend-kit 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 gatedpath and contributors
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 OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,162 @@
1
+ # @gatedpath/frontend-kit
2
+
3
+ ```
4
+ For agents
5
+ - One hook: `useEligibility(address, verifier, requestId, options?)`. It reads the network gate
6
+ (`permission.isAllowed`) and the dApp's gate (`verifier.isEligible`), caches both, refetches on
7
+ a new block or every 30 s, and exposes `refresh()`. Don't poll the RPC yourself around it.
8
+ - Five states, one component each, or `<EligibilityGate>` to pick for you: `disconnected`,
9
+ `unverified`, `ineligible`, `eligible`, `expiring` (plus `checking` and `error`). The gate is the
10
+ contract; these components decide what a button says, never whether a transaction may run.
11
+ - `expiring` only shows when the verifier can tell. `ReceptorMock` can (`receptorMockExpiry`);
12
+ real verifiers can't, so leave `expiry` unset on mainnet.
13
+ - Never suggest anvil's default accounts anywhere: they pass `isAllowed` on the real networks
14
+ (RESEARCH.md question 30). `test/hygiene.test.ts` fails the build if one appears.
15
+ - Show `<GasWarning>` near the first transaction; docs/wallet-gas-warning.md says why.
16
+ - `explainError(error)` turns a viem or wagmi error into the plain words from the shared failure
17
+ table (`@gatedpath/agent-rules/failures`): NotEligible (with the credential state when you pass
18
+ one), a wallet rejection, a wrong chain, not enough RBNT. Show `plainWords` and `fix.link`, never
19
+ `error.message` alone; `kind: "unknown"` carries the error's own words in `raw`.
20
+ - Build: `npm run build`. Tests: `npm test` (Vitest, jsdom, a mocked transport; no network).
21
+ ```
22
+
23
+ The frontend kit from `PLAN.md` section 5.6: React components for the states every Redbelly dApp
24
+ has and most will get wrong, and a hook that reads eligibility once and caches it so the UI does
25
+ not hammer the RPC. wagmi 3 and viem 2 as peers, headless with a small default style that takes
26
+ its colours, radii and type from the site's design tokens.
27
+
28
+ ## The hook
29
+
30
+ ```tsx
31
+ import { useEligibility } from '@gatedpath/frontend-kit';
32
+
33
+ const eligibility = useEligibility(address, verifier, requestId, {
34
+ refreshInterval: 30_000, // floor; every new block also refetches
35
+ watchBlocks: true, // eth_blockNumber polled by wagmi, reads refetched only when it changes
36
+ expiry: receptorMockExpiry(publicClient, verifier), // testnet and forks only
37
+ expiringWindowSeconds: 7 * 86_400,
38
+ });
39
+ // eligibility.state, .isAllowed, .isEligible, .expiresAt, .secondsToExpiry,
40
+ // .lastCheckedAt, .blockNumber, .isFetching, .error, .refresh()
41
+ ```
42
+
43
+ Two reads, both `view`: the network's permission contract (address from
44
+ `@gatedpath/chains` for the current chain, or `options.permission`) and the dApp's verifier
45
+ behind `IRedbellyVerifier.isEligible(wallet, requestId)` from receptor-mock. wagmi batches them
46
+ through Multicall3, which is deployed on both networks, so one `eth_call` per refetch. Results are
47
+ cached by wagmi's query client under the wallet, verifier, request id and chain, so every
48
+ component asking the same question shares one read.
49
+
50
+ Why block-or-30-seconds: blocks on Redbelly are produced on demand (RESEARCH.md question 4). A
51
+ busy chain gives you a block every few seconds and the reads follow it; a quiet chain gives you
52
+ none, and then a credential that was revoked off-chain would never show until the next block, so
53
+ the interval is the floor. `refresh()` is for the moment after the user comes back from
54
+ access.redbelly.network.
55
+
56
+ State derivation, in order: no address is `disconnected`; nothing cached and an RPC error is
57
+ `error`; nothing cached yet is `checking`; `isAllowed` false is `unverified` whatever the verifier
58
+ says; `isEligible` false is `ineligible`; `isEligible` true with an expiry inside the window is
59
+ `expiring`; otherwise `eligible`.
60
+
61
+ ## The components
62
+
63
+ | Component | State | Says |
64
+ |---|---|---|
65
+ | `NotConnected` | `disconnected` | Connect a wallet; identity is per wallet here |
66
+ | `NotVerified` | `unverified` | The wallet is not allowed to transact; link to access.redbelly.network, the faucet on testnet, don't switch wallets |
67
+ | `Ineligible` | `ineligible` | Verified on the network but no accepted proof for this action; the answer is a plain yes or no |
68
+ | `Eligible` | `eligible` | Wraps your action |
69
+ | `ExpiringSoon` | `expiring` | Wraps your action and says when the credential expires |
70
+ | `Checking`, `EligibilityError` | `checking`, `error` | In flight; the RPC failed and nothing is cached, with a retry |
71
+ | `EligibilityGate` | any | Picks one of the above from the hook result; `render` overrides any state |
72
+ | `LastChecked` | | "Last checked at block N" with a refresh button |
73
+ | `GasWarning` | | Vine's wallet gas warning, with a live USD figure if you pass one |
74
+
75
+ Each renders plain elements with `rb-elig-*` class names. Import
76
+ `@gatedpath/frontend-kit/styles.css` for the default look, which reads the site's
77
+ `--rb-*` and `--sl-*` tokens with their light values as fallbacks, so it follows the site's dark
78
+ theme when `tokens.css` is loaded and looks the same on its own. Or skip the stylesheet and style
79
+ the class names.
80
+
81
+ ## A page
82
+
83
+ ```tsx
84
+ const { address } = useConnection();
85
+ const eligibility = useEligibility(address, verifier, requestId);
86
+ return (
87
+ <>
88
+ <EligibilityGate eligibility={eligibility} address={address} chainName={chain.name} testnet={chain.testnet}>
89
+ <button onClick={subscribe}>Subscribe</button>
90
+ </EligibilityGate>
91
+ <LastChecked eligibility={eligibility} />
92
+ <GasWarning transferUsd={usd} />
93
+ </>
94
+ );
95
+ ```
96
+
97
+ The scaffolder's `web/` app has this as a working page at `/eligibility`
98
+ (`templates/web/src/app/eligibility/page.tsx`): it reads the template contract's `verifier()` and
99
+ `requestId()`, uses `receptorMockExpiry` on testnet, and prices the gas figure from the on-chain
100
+ feed with `gasCostUsd` from `@gatedpath/chains`.
101
+
102
+ ## The dev entry: `@gatedpath/frontend-kit/dev`
103
+
104
+ The local loop from PLAN.md 18.1. `DevStatePanel` lists the five wallets `npm run dev` seeded
105
+ in a scaffold, marks the connected one, and flips the connected wallet through NeverIssued,
106
+ Valid, Expired, Revoked and WrongJurisdiction by calling `setStatus` on the `ReceptorMock` as the
107
+ deployer through Anvil's impersonation RPC (`anvil_impersonateAccount`, then stop). It fires
108
+ `notifyEligibilityChanged(wallet)`, which every mounted `useEligibility` for that wallet listens
109
+ to, so the page moves into the new state without a reload or a new block. `anvilAccountConnector`
110
+ is a wagmi connector for one of Anvil's unlocked accounts: the node signs, the browser never holds
111
+ a key. Its style is `@gatedpath/frontend-kit/dev.css`, kept apart from `styles.css` so a
112
+ production bundle that loads the main stylesheet carries nothing of the panel. `parseLocalDeployment` reads `deployments/local.json` as `scripts/dev.mjs` writes it and
113
+ refuses anything that isn't chain 31337 with five wallets on accounts 2 or later.
114
+
115
+ The panel renders only on chain 31337. On 151 or 153 it renders nothing whatever it is given,
116
+ and the scaffold imports it only in the dev server behind a build-time constant, so a production
117
+ bundle carries none of it; the scaffolder's integration test builds one and checks.
118
+
119
+ ## Tests
120
+
121
+ `npm test`: 80 Vitest tests in jsdom. `test/mock-chain.tsx` is a wagmi config over a `custom`
122
+ transport that answers `eth_chainId`, `eth_blockNumber`, `eth_call` and Multicall3 `aggregate3`
123
+ from an in-memory state the tests mutate; nothing leaves the process and no address in it exists
124
+ anywhere. The hook tests cover every state, that reads happen once and then only on a new block,
125
+ manual refresh without a block, shared reads, the expiry window, errors and `enabled: false`. The
126
+ component tests render every state, the gate's selection and override, the links (Access dApp,
127
+ faucet, Vine) and the retry and refresh buttons. `test/dev-panel.test.tsx` runs the dev panel
128
+ against a fake chain 31337 (and proves it renders nothing on 153 or 151), including the
129
+ impersonate, send, stop sequence and the refetch without a new block. `test/hygiene.test.ts` walks every file in the
130
+ package and fails on any of anvil's ten default accounts or any 64-hex value.
131
+
132
+ Not tested: a real wallet. Nothing here signs; the kit reads and renders.
133
+
134
+ ## Not in v1
135
+
136
+ Storybook (optional in the brief, skipped). A connect button (wagmi's own connectors and the
137
+ scaffolder's `Connect` component do that). Anything that calls the Eligibility SDK: the kit tells
138
+ the user they are ineligible, and the SDK is the thing that can tell them why; the scaffolder's
139
+ `web/src/eligibility/` has the steps for adding it.
140
+
141
+ ## explainError
142
+
143
+ ```tsx
144
+ import { explainError } from '@gatedpath/frontend-kit';
145
+
146
+ const write = useWriteContract();
147
+ // …
148
+ {write.error && <p>{explainError(write.error).plainWords}</p>}
149
+ ```
150
+
151
+ `explainError(error, { eligibilityStatus? })` walks the error's `cause` chain and answers
152
+ `{ kind, plainWords, cause, fix: { command?, link? }, title?, docsPage?, notEligible?, selector?, raw }`
153
+ from the failure table in `@gatedpath/agent-rules/failures`, the same table the MCP server's
154
+ `explain_failure` and the site's `/errors` page read. It decodes a `ContractFunctionRevertedError`
155
+ by its revert data (a `NotEligible(wallet, requestId)` selector, `ZeroVerifier`, or an
156
+ `Error(string)` reason from the deploy script) or by the error name viem decoded, and recognises
157
+ `UserRejectedRequestError` (or an EIP-1193 code 4001), `ChainMismatchError`, `SwitchChainError` and
158
+ `InsufficientFundsError` by class wherever they sit in the chain. Pass `eligibilityStatus` when the
159
+ page already knows the state behind a `NotEligible` (a `ReceptorMock` can say; a real verifier
160
+ can't) and the entry for that state comes back. Anything else is `kind: "unknown"` with the
161
+ error's own message in `raw`, never a guess. Pure: no network, no React. The scaffold's web app
162
+ renders it in `web/src/components/FailureWords.tsx` under the gated button and the eligibility page.
package/dist/abi.d.ts ADDED
@@ -0,0 +1,98 @@
1
+ /** `IRedbellyVerifier.isEligible(address, uint64)` from @gatedpath/receptor-mock. */
2
+ export declare const verifierAbi: readonly [{
3
+ readonly type: "function";
4
+ readonly name: "isEligible";
5
+ readonly stateMutability: "view";
6
+ readonly inputs: readonly [{
7
+ readonly name: "wallet";
8
+ readonly type: "address";
9
+ }, {
10
+ readonly name: "requestId";
11
+ readonly type: "uint64";
12
+ }];
13
+ readonly outputs: readonly [{
14
+ readonly name: "";
15
+ readonly type: "bool";
16
+ }];
17
+ }];
18
+ /** The network's permission contract: `isAllowed(address)` (RESEARCH.md question 7). */
19
+ export declare const permissionAbi: readonly [{
20
+ readonly type: "function";
21
+ readonly name: "isAllowed";
22
+ readonly stateMutability: "view";
23
+ readonly inputs: readonly [{
24
+ readonly name: "wallet";
25
+ readonly type: "address";
26
+ }];
27
+ readonly outputs: readonly [{
28
+ readonly name: "";
29
+ readonly type: "bool";
30
+ }];
31
+ }];
32
+ /**
33
+ * `ReceptorMock.recordFor(address, uint64)` from @gatedpath/receptor-mock. The mock is the
34
+ * only verifier in the kit that can say when a credential expires; real verifiers answer a plain
35
+ * boolean. Used by `receptorMockExpiry` for testnet and fork demos only.
36
+ */
37
+ export declare const receptorMockAbi: readonly [{
38
+ readonly type: "function";
39
+ readonly name: "recordFor";
40
+ readonly stateMutability: "view";
41
+ readonly inputs: readonly [{
42
+ readonly name: "wallet";
43
+ readonly type: "address";
44
+ }, {
45
+ readonly name: "requestId";
46
+ readonly type: "uint64";
47
+ }];
48
+ readonly outputs: readonly [{
49
+ readonly name: "";
50
+ readonly type: "tuple";
51
+ readonly components: readonly [{
52
+ readonly name: "isSet";
53
+ readonly type: "bool";
54
+ }, {
55
+ readonly name: "status";
56
+ readonly type: "uint8";
57
+ }, {
58
+ readonly name: "expiresAt";
59
+ readonly type: "uint64";
60
+ }];
61
+ }];
62
+ }];
63
+ /**
64
+ * The two `ReceptorMock` functions the dev state panel uses on a local Anvil (chain 31337):
65
+ * `setStatus(wallet, requestId, status)` writes a state, `eligibilityStatus(wallet, requestId)`
66
+ * reads it back. Never called against 151 or 153; the panel refuses to render there.
67
+ */
68
+ export declare const receptorMockStatusAbi: readonly [{
69
+ readonly type: "function";
70
+ readonly name: "setStatus";
71
+ readonly stateMutability: "nonpayable";
72
+ readonly inputs: readonly [{
73
+ readonly name: "wallet";
74
+ readonly type: "address";
75
+ }, {
76
+ readonly name: "requestId";
77
+ readonly type: "uint64";
78
+ }, {
79
+ readonly name: "status";
80
+ readonly type: "uint8";
81
+ }];
82
+ readonly outputs: readonly [];
83
+ }, {
84
+ readonly type: "function";
85
+ readonly name: "eligibilityStatus";
86
+ readonly stateMutability: "view";
87
+ readonly inputs: readonly [{
88
+ readonly name: "wallet";
89
+ readonly type: "address";
90
+ }, {
91
+ readonly name: "requestId";
92
+ readonly type: "uint64";
93
+ }];
94
+ readonly outputs: readonly [{
95
+ readonly name: "";
96
+ readonly type: "uint8";
97
+ }];
98
+ }];
package/dist/abi.js ADDED
@@ -0,0 +1,79 @@
1
+ // The two reads the kit makes. Nothing else is called.
2
+ /** `IRedbellyVerifier.isEligible(address, uint64)` from @gatedpath/receptor-mock. */
3
+ export const verifierAbi = [
4
+ {
5
+ type: 'function',
6
+ name: 'isEligible',
7
+ stateMutability: 'view',
8
+ inputs: [
9
+ { name: 'wallet', type: 'address' },
10
+ { name: 'requestId', type: 'uint64' },
11
+ ],
12
+ outputs: [{ name: '', type: 'bool' }],
13
+ },
14
+ ];
15
+ /** The network's permission contract: `isAllowed(address)` (RESEARCH.md question 7). */
16
+ export const permissionAbi = [
17
+ {
18
+ type: 'function',
19
+ name: 'isAllowed',
20
+ stateMutability: 'view',
21
+ inputs: [{ name: 'wallet', type: 'address' }],
22
+ outputs: [{ name: '', type: 'bool' }],
23
+ },
24
+ ];
25
+ /**
26
+ * `ReceptorMock.recordFor(address, uint64)` from @gatedpath/receptor-mock. The mock is the
27
+ * only verifier in the kit that can say when a credential expires; real verifiers answer a plain
28
+ * boolean. Used by `receptorMockExpiry` for testnet and fork demos only.
29
+ */
30
+ export const receptorMockAbi = [
31
+ {
32
+ type: 'function',
33
+ name: 'recordFor',
34
+ stateMutability: 'view',
35
+ inputs: [
36
+ { name: 'wallet', type: 'address' },
37
+ { name: 'requestId', type: 'uint64' },
38
+ ],
39
+ outputs: [
40
+ {
41
+ name: '',
42
+ type: 'tuple',
43
+ components: [
44
+ { name: 'isSet', type: 'bool' },
45
+ { name: 'status', type: 'uint8' },
46
+ { name: 'expiresAt', type: 'uint64' },
47
+ ],
48
+ },
49
+ ],
50
+ },
51
+ ];
52
+ /**
53
+ * The two `ReceptorMock` functions the dev state panel uses on a local Anvil (chain 31337):
54
+ * `setStatus(wallet, requestId, status)` writes a state, `eligibilityStatus(wallet, requestId)`
55
+ * reads it back. Never called against 151 or 153; the panel refuses to render there.
56
+ */
57
+ export const receptorMockStatusAbi = [
58
+ {
59
+ type: 'function',
60
+ name: 'setStatus',
61
+ stateMutability: 'nonpayable',
62
+ inputs: [
63
+ { name: 'wallet', type: 'address' },
64
+ { name: 'requestId', type: 'uint64' },
65
+ { name: 'status', type: 'uint8' },
66
+ ],
67
+ outputs: [],
68
+ },
69
+ {
70
+ type: 'function',
71
+ name: 'eligibilityStatus',
72
+ stateMutability: 'view',
73
+ inputs: [
74
+ { name: 'wallet', type: 'address' },
75
+ { name: 'requestId', type: 'uint64' },
76
+ ],
77
+ outputs: [{ name: '', type: 'uint8' }],
78
+ },
79
+ ];
@@ -0,0 +1,70 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { Address, EligibilityResult, EligibilityState } from './useEligibility.js';
3
+ export interface StateProps {
4
+ className?: string;
5
+ children?: ReactNode;
6
+ }
7
+ /** State 1: nothing is connected. */
8
+ export declare function NotConnected({ className, children }: StateProps): import("react/jsx-runtime").JSX.Element;
9
+ export interface NotVerifiedProps extends StateProps {
10
+ address: Address;
11
+ /** Human name of the chain, for the sentence. */
12
+ chainName?: string;
13
+ /** True on chain 153: shows the faucet link. */
14
+ testnet?: boolean;
15
+ }
16
+ /** State 2: connected, but `permission.isAllowed` is false. The wallet cannot transact at all. */
17
+ export declare function NotVerified({ address, chainName, testnet, className, children }: NotVerifiedProps): import("react/jsx-runtime").JSX.Element;
18
+ export interface IneligibleProps extends StateProps {
19
+ /** What the action needs, in plain words: "an Australian proof of address", "an over-18 credential". */
20
+ requirement?: string;
21
+ }
22
+ /** State 3: verified on the network, but the verifier says no for this action. */
23
+ export declare function Ineligible({ requirement, className, children }: IneligibleProps): import("react/jsx-runtime").JSX.Element;
24
+ /** State 4: eligible. Usually wraps the action button. */
25
+ export declare function Eligible({ className, children }: StateProps): import("react/jsx-runtime").JSX.Element;
26
+ export interface ExpiringSoonProps extends StateProps {
27
+ expiresAt: number;
28
+ secondsToExpiry: number;
29
+ }
30
+ /** State 5: eligible, and the verifier says the credential expires soon. Wraps the action too. */
31
+ export declare function ExpiringSoon({ expiresAt, secondsToExpiry, className, children }: ExpiringSoonProps): import("react/jsx-runtime").JSX.Element;
32
+ /** Shown while both reads are in flight for the first time. */
33
+ export declare function Checking({ className, children }: StateProps): import("react/jsx-runtime").JSX.Element;
34
+ export interface EligibilityErrorProps extends StateProps {
35
+ error: Error | null;
36
+ onRetry?: () => void;
37
+ }
38
+ /** An RPC or contract read failed and nothing is cached. Says so; does not guess. */
39
+ export declare function EligibilityError({ error, onRetry, className, children }: EligibilityErrorProps): import("react/jsx-runtime").JSX.Element;
40
+ export interface EligibilityGateProps {
41
+ /** The result of `useEligibility`. */
42
+ eligibility: EligibilityResult;
43
+ address: Address | undefined;
44
+ chainName?: string;
45
+ testnet?: boolean;
46
+ requirement?: string;
47
+ className?: string;
48
+ /** Rendered inside the eligible and expiring states: the action itself. */
49
+ children?: ReactNode;
50
+ /** Override any state with your own markup. A function receives the hook result. */
51
+ render?: Partial<Record<EligibilityState, ReactNode | ((r: EligibilityResult) => ReactNode)>>;
52
+ }
53
+ /** One component that picks the state component for you. Bring your own action as `children`. */
54
+ export declare function EligibilityGate({ eligibility, address, chainName, testnet, requirement, className, children, render }: EligibilityGateProps): import("react/jsx-runtime").JSX.Element;
55
+ export interface LastCheckedProps {
56
+ eligibility: EligibilityResult;
57
+ className?: string;
58
+ }
59
+ /** "Last checked" line with a refresh button. Put it under any gate. */
60
+ export declare function LastChecked({ eligibility, className }: LastCheckedProps): import("react/jsx-runtime").JSX.Element;
61
+ export interface GasWarningProps {
62
+ /** A live USD figure for a plain transfer, from `gasCostUsd` in @gatedpath/chains, if you have one. */
63
+ transferUsd?: string | null;
64
+ className?: string;
65
+ }
66
+ /**
67
+ * The wallet gas warning from Vine, which every Redbelly dApp should show near its first
68
+ * transaction. See docs/wallet-gas-warning.md for the source and the reasoning.
69
+ */
70
+ export declare function GasWarning({ transferUsd, className }: GasWarningProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,85 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { ACCESS_URL, FAUCET_URL, VINE_FEES_URL, VINE_INTERACTION_URL } from './urls.js';
4
+ function cx(...parts) {
5
+ return parts.filter(Boolean).join(' ');
6
+ }
7
+ function short(address) {
8
+ return `${address.slice(0, 6)}…${address.slice(-4)}`;
9
+ }
10
+ /** State 1: nothing is connected. */
11
+ export function NotConnected({ className, children }) {
12
+ return (_jsxs("div", { className: cx('rb-elig', 'rb-elig-disconnected', className), role: "status", children: [_jsx("p", { className: "rb-elig-title", children: "Connect a wallet" }), _jsx("p", { className: "rb-elig-body", children: children ?? 'Redbelly checks identity per wallet, so the app needs a wallet before it can tell you anything.' })] }));
13
+ }
14
+ /** State 2: connected, but `permission.isAllowed` is false. The wallet cannot transact at all. */
15
+ export function NotVerified({ address, chainName = 'this network', testnet = false, className, children }) {
16
+ return (_jsxs("div", { className: cx('rb-elig', 'rb-elig-unverified', className), role: "status", children: [_jsx("p", { className: "rb-elig-title", children: "Verify your wallet" }), _jsxs("p", { className: "rb-elig-body", children: [_jsx("code", { children: short(address) }), " is not yet allowed to transact on ", chainName, ". Redbelly gates every wallet behind a one-time identity check; this app never sees your documents, only a true or false from the network."] }), _jsxs("ol", { className: "rb-elig-steps", children: [_jsxs("li", { children: ["Open", ' ', _jsx("a", { href: ACCESS_URL, target: "_blank", rel: "noreferrer", children: ACCESS_URL }), ' ', "with this same wallet and complete verification."] }), testnet && (_jsxs("li", { children: ["Get testnet RBNT from", ' ', _jsx("a", { href: FAUCET_URL, target: "_blank", rel: "noreferrer", children: "FAUCETME" }), ' ', "so you can pay gas."] })), _jsx("li", { children: "Come back here. This page re-checks on every new block and moves on by itself." })] }), _jsx("p", { className: "rb-elig-muted", children: children ?? "Don't switch to another wallet to get past this: the credential belongs to the wallet that was verified." })] }));
17
+ }
18
+ /** State 3: verified on the network, but the verifier says no for this action. */
19
+ export function Ineligible({ requirement, className, children }) {
20
+ return (_jsxs("div", { className: cx('rb-elig', 'rb-elig-ineligible', className), role: "status", children: [_jsx("p", { className: "rb-elig-title", children: "Not eligible for this action" }), _jsx("p", { className: "rb-elig-body", children: children ??
21
+ `Your wallet is verified on the network, but it does not hold an accepted proof for this action${requirement ? ` (${requirement})` : ''}. The contract would refuse the transaction, so the button stays off.` }), _jsx("p", { className: "rb-elig-muted", children: "The check is on-chain and the answer is a plain yes or no; the app cannot tell whether a credential is missing, expired or revoked. Your identity wallet can." })] }));
22
+ }
23
+ /** State 4: eligible. Usually wraps the action button. */
24
+ export function Eligible({ className, children }) {
25
+ return (_jsxs("div", { className: cx('rb-elig', 'rb-elig-eligible', className), role: "status", children: [_jsx("p", { className: "rb-elig-title", children: "Eligible" }), children] }));
26
+ }
27
+ function humanDuration(seconds) {
28
+ if (seconds <= 0)
29
+ return 'now';
30
+ const days = Math.floor(seconds / 86_400);
31
+ if (days >= 2)
32
+ return `${days} days`;
33
+ const hours = Math.floor(seconds / 3600);
34
+ if (hours >= 2)
35
+ return `${hours} hours`;
36
+ const minutes = Math.max(1, Math.floor(seconds / 60));
37
+ return `${minutes} minutes`;
38
+ }
39
+ /** State 5: eligible, and the verifier says the credential expires soon. Wraps the action too. */
40
+ export function ExpiringSoon({ expiresAt, secondsToExpiry, className, children }) {
41
+ const when = new Date(expiresAt * 1000);
42
+ return (_jsxs("div", { className: cx('rb-elig', 'rb-elig-expiring', className), role: "status", children: [_jsxs("p", { className: "rb-elig-title", children: ["Eligible, credential expires in ", humanDuration(secondsToExpiry)] }), _jsxs("p", { className: "rb-elig-body", children: ["After ", _jsx("time", { dateTime: when.toISOString(), children: when.toUTCString() }), " this wallet will be refused until the credential is renewed. Renew it in your identity wallet before then; a transaction sent after expiry reverts."] }), children] }));
43
+ }
44
+ /** Shown while both reads are in flight for the first time. */
45
+ export function Checking({ className, children }) {
46
+ return (_jsx("div", { className: cx('rb-elig', 'rb-elig-checking', className), role: "status", "aria-busy": "true", children: _jsx("p", { className: "rb-elig-body", children: children ?? 'Checking the network and the verifier…' }) }));
47
+ }
48
+ /** An RPC or contract read failed and nothing is cached. Says so; does not guess. */
49
+ export function EligibilityError({ error, onRetry, className, children }) {
50
+ return (_jsxs("div", { className: cx('rb-elig', 'rb-elig-error', className), role: "alert", children: [_jsx("p", { className: "rb-elig-title", children: "Could not check eligibility" }), _jsx("p", { className: "rb-elig-body", children: children ?? (error?.message ?? 'The RPC did not answer.') }), onRetry && (_jsx("button", { type: "button", className: "rb-elig-button", onClick: onRetry, children: "Try again" }))] }));
51
+ }
52
+ /** One component that picks the state component for you. Bring your own action as `children`. */
53
+ export function EligibilityGate({ eligibility, address, chainName, testnet, requirement, className, children, render }) {
54
+ const custom = render?.[eligibility.state];
55
+ if (custom !== undefined)
56
+ return _jsx(_Fragment, { children: typeof custom === 'function' ? custom(eligibility) : custom });
57
+ switch (eligibility.state) {
58
+ case 'disconnected':
59
+ return _jsx(NotConnected, { className: className });
60
+ case 'checking':
61
+ return _jsx(Checking, { className: className });
62
+ case 'unverified':
63
+ return _jsx(NotVerified, { address: address, chainName: chainName, testnet: testnet, className: className });
64
+ case 'ineligible':
65
+ return _jsx(Ineligible, { requirement: requirement, className: className });
66
+ case 'expiring':
67
+ return (_jsx(ExpiringSoon, { expiresAt: eligibility.expiresAt, secondsToExpiry: eligibility.secondsToExpiry, className: className, children: children }));
68
+ case 'eligible':
69
+ return _jsx(Eligible, { className: className, children: children });
70
+ case 'error':
71
+ return _jsx(EligibilityError, { error: eligibility.error, onRetry: () => void eligibility.refresh(), className: className });
72
+ }
73
+ }
74
+ /** "Last checked" line with a refresh button. Put it under any gate. */
75
+ export function LastChecked({ eligibility, className }) {
76
+ const at = eligibility.lastCheckedAt;
77
+ return (_jsxs("p", { className: cx('rb-elig-last-checked', className), children: [at ? (_jsxs(_Fragment, { children: ["Last checked ", _jsx("time", { dateTime: new Date(at).toISOString(), children: new Date(at).toLocaleTimeString() }), eligibility.blockNumber !== undefined ? ` at block ${eligibility.blockNumber.toString()}` : ''] })) : ('Not checked yet'), _jsx("button", { type: "button", className: "rb-elig-button rb-elig-button-small", onClick: () => void eligibility.refresh(), disabled: eligibility.isFetching, children: eligibility.isFetching ? 'Checking…' : 'Refresh' })] }));
78
+ }
79
+ /**
80
+ * The wallet gas warning from Vine, which every Redbelly dApp should show near its first
81
+ * transaction. See docs/wallet-gas-warning.md for the source and the reasoning.
82
+ */
83
+ export function GasWarning({ transferUsd, className }) {
84
+ return (_jsxs("p", { className: cx('rb-elig-gas', className), children: ["Gas on Redbelly is priced in US dollars and paid in RBNT", transferUsd ? `; a plain transfer costs about US$${transferUsd} right now` : '', ". Redbelly warns that most wallets show gas information wrongly on this network, so trust the figure here over the wallet's, and read", ' ', _jsx("a", { href: VINE_INTERACTION_URL, target: "_blank", rel: "noreferrer", children: "Vine's note" }), ' ', "and", ' ', _jsx("a", { href: VINE_FEES_URL, target: "_blank", rel: "noreferrer", children: "how fees work" }), "."] }));
85
+ }
@@ -0,0 +1,19 @@
1
+ import { type LocalDeployment } from './localDeployment.js';
2
+ export interface DevStatePanelProps {
3
+ /** The parsed `deployments/local.json`; see `parseLocalDeployment`. */
4
+ deployment: LocalDeployment;
5
+ className?: string;
6
+ }
7
+ /**
8
+ * The dev-only state switcher (PLAN.md 18.1). Lists the five wallets `npm run dev` seeded, marks
9
+ * the connected one, and flips the connected wallet through the five credential states by calling
10
+ * `setStatus` on the `ReceptorMock` as the deployer, through Anvil's impersonation RPC. Every
11
+ * mounted `useEligibility` for that wallet refetches on the resulting signal, so the page moves
12
+ * into the new state without a reload.
13
+ *
14
+ * It renders only when the wallet (or, unconnected, the app) is on chain 31337 and the deployment
15
+ * it was given says 31337 too. On 151 or 153 it renders nothing, whatever it was given. In a
16
+ * scaffold it is imported only by the dev server and is absent from production bundles; the
17
+ * scaffolder's test asserts both.
18
+ */
19
+ export declare function DevStatePanel({ deployment, className }: DevStatePanelProps): import("react/jsx-runtime").JSX.Element | null;