@solana/kit-plugin-wallet 0.0.0 → 0.13.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.
Files changed (44) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +401 -0
  3. package/dist/index.browser.cjs +622 -0
  4. package/dist/index.browser.cjs.map +1 -0
  5. package/dist/index.browser.mjs +614 -0
  6. package/dist/index.browser.mjs.map +1 -0
  7. package/dist/index.node.cjs +100 -0
  8. package/dist/index.node.cjs.map +1 -0
  9. package/dist/index.node.mjs +91 -0
  10. package/dist/index.node.mjs.map +1 -0
  11. package/dist/index.react-native.mjs +91 -0
  12. package/dist/index.react-native.mjs.map +1 -0
  13. package/dist/react/index.browser.cjs +77 -0
  14. package/dist/react/index.browser.cjs.map +1 -0
  15. package/dist/react/index.browser.mjs +59 -0
  16. package/dist/react/index.browser.mjs.map +1 -0
  17. package/dist/react/index.node.cjs +77 -0
  18. package/dist/react/index.node.cjs.map +1 -0
  19. package/dist/react/index.node.mjs +59 -0
  20. package/dist/react/index.node.mjs.map +1 -0
  21. package/dist/react/index.react-native.mjs +59 -0
  22. package/dist/react/index.react-native.mjs.map +1 -0
  23. package/dist/types/index.d.ts +4 -0
  24. package/dist/types/index.d.ts.map +1 -0
  25. package/dist/types/react/index.d.ts +235 -0
  26. package/dist/types/react/index.d.ts.map +1 -0
  27. package/dist/types/status.d.ts +31 -0
  28. package/dist/types/status.d.ts.map +1 -0
  29. package/dist/types/store.d.ts +16 -0
  30. package/dist/types/store.d.ts.map +1 -0
  31. package/dist/types/types.d.ts +384 -0
  32. package/dist/types/types.d.ts.map +1 -0
  33. package/dist/types/wallet.d.ts +133 -0
  34. package/dist/types/wallet.d.ts.map +1 -0
  35. package/package.json +103 -7
  36. package/src/__typetests__/wallet-typetest.ts +129 -0
  37. package/src/index.ts +3 -0
  38. package/src/react/__typetests__/index.ts +108 -0
  39. package/src/react/index.ts +301 -0
  40. package/src/status.ts +33 -0
  41. package/src/store.ts +990 -0
  42. package/src/types/global.d.ts +6 -0
  43. package/src/types.ts +407 -0
  44. package/src/wallet.ts +218 -0
@@ -0,0 +1,129 @@
1
+ import {
2
+ type ClientWithIdentity,
3
+ type ClientWithPayer,
4
+ type ClientWithSubscribeToIdentity,
5
+ type ClientWithSubscribeToPayer,
6
+ createClient,
7
+ TransactionSigner,
8
+ } from '@solana/kit';
9
+
10
+ import { isWalletWarmingUp, type WalletStatus } from '../index';
11
+ import { ClientWithWallet } from '../types';
12
+ import { walletIdentity, walletPayer, walletSigner, walletWithoutSigner } from '../wallet';
13
+
14
+ const config = { chain: 'solana:mainnet' as const };
15
+
16
+ const signer = null as unknown as TransactionSigner;
17
+
18
+ // [DESCRIBE] walletSigner
19
+ {
20
+ // It sets payer, identity, and wallet on the client.
21
+ {
22
+ const client = createClient().use(walletSigner(config));
23
+ client.payer satisfies ClientWithPayer['payer'];
24
+ client.identity satisfies ClientWithIdentity['identity'];
25
+ client.wallet satisfies ClientWithWallet['wallet'];
26
+ client.subscribeToPayer satisfies ClientWithSubscribeToPayer['subscribeToPayer'];
27
+ client.subscribeToIdentity satisfies ClientWithSubscribeToIdentity['subscribeToIdentity'];
28
+ }
29
+ }
30
+
31
+ // [DESCRIBE] walletPayer
32
+ {
33
+ // It sets payer and wallet on the client.
34
+ {
35
+ const client = createClient().use(walletPayer(config));
36
+ client.payer satisfies ClientWithPayer['payer'];
37
+ client.wallet satisfies ClientWithWallet['wallet'];
38
+ client.subscribeToPayer satisfies ClientWithSubscribeToPayer['subscribeToPayer'];
39
+ // @ts-expect-error walletPayer does not install subscribeToIdentity.
40
+ void client.subscribeToIdentity;
41
+ }
42
+ // It does not strip a previously-set identity.
43
+ {
44
+ const base = { identity: signer } as unknown as ClientWithIdentity;
45
+ const result = walletPayer(config)(base);
46
+ result.identity satisfies TransactionSigner;
47
+ }
48
+ }
49
+
50
+ // [DESCRIBE] walletIdentity
51
+ {
52
+ // It sets identity and wallet on the client.
53
+ {
54
+ const client = createClient().use(walletIdentity(config));
55
+ client.identity satisfies ClientWithIdentity['identity'];
56
+ client.wallet satisfies ClientWithWallet['wallet'];
57
+ client.subscribeToIdentity satisfies ClientWithSubscribeToIdentity['subscribeToIdentity'];
58
+ // @ts-expect-error walletIdentity does not install subscribeToPayer.
59
+ void client.subscribeToPayer;
60
+ }
61
+ // It does not strip a previously-set payer.
62
+ {
63
+ const base = { payer: signer } as unknown as ClientWithPayer;
64
+ const result = walletIdentity(config)(base);
65
+ result.payer satisfies TransactionSigner;
66
+ }
67
+ }
68
+
69
+ // [DESCRIBE] walletWithoutSigner
70
+ {
71
+ // It sets wallet on the client.
72
+ {
73
+ const client = createClient().use(walletWithoutSigner(config));
74
+ client.wallet satisfies ClientWithWallet['wallet'];
75
+ // @ts-expect-error walletWithoutSigner does not install subscribeToPayer.
76
+ void client.subscribeToPayer;
77
+ // @ts-expect-error walletWithoutSigner does not install subscribeToIdentity.
78
+ void client.subscribeToIdentity;
79
+ }
80
+ // It does not strip a previously-set payer.
81
+ {
82
+ const base = { payer: signer } as unknown as ClientWithPayer;
83
+ const result = walletWithoutSigner(config)(base);
84
+ result.payer satisfies TransactionSigner;
85
+ }
86
+ // It does not strip a previously-set identity.
87
+ {
88
+ const base = { identity: signer } as unknown as ClientWithIdentity;
89
+ const result = walletWithoutSigner(config)(base);
90
+ result.identity satisfies TransactionSigner;
91
+ }
92
+ // It does not strip a previously-set payer and identity.
93
+ {
94
+ const base = { identity: signer, payer: signer } as unknown as ClientWithIdentity & ClientWithPayer;
95
+ const result = walletWithoutSigner(config)(base);
96
+ result.payer satisfies TransactionSigner;
97
+ result.identity satisfies TransactionSigner;
98
+ }
99
+ }
100
+
101
+ // [DESCRIBE] isWalletWarmingUp
102
+ {
103
+ // It is exported from the root entry and narrows a status to a boolean.
104
+ {
105
+ const status = null as unknown as WalletStatus;
106
+ isWalletWarmingUp(status) satisfies boolean;
107
+ }
108
+ // It requires a WalletStatus argument.
109
+ {
110
+ // @ts-expect-error isWalletWarmingUp requires a status argument.
111
+ isWalletWarmingUp();
112
+ }
113
+ }
114
+
115
+ // [DESCRIBE] Only one wallet plugin allowed
116
+ {
117
+ // It fails to typecheck when a wallet plugin is used on a client that already has wallet.
118
+ {
119
+ const client = createClient().use(walletSigner(config));
120
+ // @ts-expect-error Cannot use a second wallet plugin.
121
+ walletSigner(config)(client);
122
+ // @ts-expect-error Cannot use a second wallet plugin.
123
+ walletPayer(config)(client);
124
+ // @ts-expect-error Cannot use a second wallet plugin.
125
+ walletIdentity(config)(client);
126
+ // @ts-expect-error Cannot use a second wallet plugin.
127
+ walletWithoutSigner(config)(client);
128
+ }
129
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './status';
2
+ export * from './types';
3
+ export * from './wallet';
@@ -0,0 +1,108 @@
1
+ import type { ReadonlyUint8Array, SignatureBytes } from '@solana/kit';
2
+ import type { SolanaSignInInput, SolanaSignInOutput } from '@solana/wallet-standard-features';
3
+ import type { UiWallet, UiWalletAccount } from '@wallet-standard/ui';
4
+ import type { ReactNode } from 'react';
5
+
6
+ import type { WalletState, WalletStatus } from '../../types';
7
+ import {
8
+ useConnect,
9
+ useConnectedWallet,
10
+ useDisconnect,
11
+ useIsWalletReady,
12
+ useSelectAccount,
13
+ useSignIn,
14
+ useSignMessage,
15
+ useWallets,
16
+ WalletReadyGate,
17
+ type WalletReadyGateProps,
18
+ useWalletStatus,
19
+ } from '../index';
20
+
21
+ // NB: hooks are only type-checked here, never executed.
22
+
23
+ // [DESCRIBE] useWalletStatus
24
+ {
25
+ const status = useWalletStatus();
26
+ status satisfies WalletStatus;
27
+ }
28
+
29
+ // [DESCRIBE] useIsWalletReady
30
+ {
31
+ const isReady = useIsWalletReady();
32
+ isReady satisfies boolean;
33
+ }
34
+
35
+ // [DESCRIBE] WalletReadyGate
36
+ {
37
+ // Both `children` and `fallback` are required `ReactNode` props.
38
+ ({ children: 'ready', fallback: 'loading' }) satisfies WalletReadyGateProps;
39
+ // The gate is callable and its return type is assignable to `ReactNode`.
40
+ const rendered: ReactNode = WalletReadyGate({ children: null, fallback: null });
41
+ void rendered;
42
+ }
43
+ {
44
+ // @ts-expect-error `fallback` is required.
45
+ ({ children: null }) satisfies WalletReadyGateProps;
46
+ }
47
+
48
+ // [DESCRIBE] useConnectedWallet
49
+ {
50
+ const connected = useConnectedWallet();
51
+ connected satisfies WalletState['connected'];
52
+ // The connection is nullable when disconnected.
53
+ connected satisfies { account: UiWalletAccount } | null;
54
+ }
55
+
56
+ // [DESCRIBE] useWallets
57
+ {
58
+ const wallets = useWallets();
59
+ wallets satisfies readonly UiWallet[];
60
+ }
61
+
62
+ // [DESCRIBE] useConnect
63
+ {
64
+ const action = useConnect();
65
+ action.dispatch(null as unknown as UiWallet);
66
+ action.data satisfies readonly UiWalletAccount[] | undefined;
67
+ // @ts-expect-error dispatch requires the wallet argument.
68
+ action.dispatch();
69
+ }
70
+
71
+ // [DESCRIBE] useDisconnect
72
+ {
73
+ const action = useDisconnect();
74
+ // The wallet argument is optional — omit it to disconnect the active wallet.
75
+ action.dispatch();
76
+ // Or pass a specific wallet to deauthorize.
77
+ action.dispatch(null as unknown as UiWallet);
78
+ action.data satisfies void | undefined;
79
+ }
80
+
81
+ // [DESCRIBE] useSignIn
82
+ {
83
+ const action = useSignIn();
84
+ action.dispatch(null as unknown as UiWallet, null as unknown as SolanaSignInInput);
85
+ action.data satisfies SolanaSignInOutput | undefined;
86
+ // @ts-expect-error dispatch requires both the wallet and the sign-in input.
87
+ action.dispatch(null as unknown as UiWallet);
88
+ }
89
+
90
+ // [DESCRIBE] useSignMessage
91
+ {
92
+ const action = useSignMessage();
93
+ action.dispatch(new Uint8Array());
94
+ action.data satisfies SignatureBytes | undefined;
95
+ }
96
+ {
97
+ // dispatch accepts a ReadonlyUint8Array (e.g. codec output) without a cast.
98
+ const action = useSignMessage();
99
+ action.dispatch(null as unknown as ReadonlyUint8Array);
100
+ }
101
+
102
+ // [DESCRIBE] useSelectAccount
103
+ {
104
+ const selectAccount = useSelectAccount();
105
+ selectAccount(null as unknown as UiWalletAccount) satisfies void;
106
+ // @ts-expect-error selectAccount requires the account argument.
107
+ selectAccount();
108
+ }
@@ -0,0 +1,301 @@
1
+ import type { ReadonlyUint8Array, SignatureBytes } from '@solana/kit';
2
+ import { type ActionResult, useAction, useClientCapability } from '@solana/react';
3
+ import type { SolanaSignInInput, SolanaSignInOutput } from '@solana/wallet-standard-features';
4
+ import type { UiWallet, UiWalletAccount } from '@wallet-standard/ui';
5
+ import type { ReactNode } from 'react';
6
+ import { useSyncExternalStore } from 'react';
7
+
8
+ import { isWalletWarmingUp } from '../status';
9
+ import type { ClientWithWallet, WalletNamespace, WalletState, WalletStatus } from '../types';
10
+
11
+ /**
12
+ * Reads the wallet namespace from the nearest `ClientProvider`, asserting at mount that a wallet
13
+ * plugin is installed. Shared by every hook in this module so the capability check, error hook
14
+ * name, and provider hint stay in one place.
15
+ */
16
+ function useWalletNamespace(hookName: string): WalletNamespace {
17
+ return useClientCapability<ClientWithWallet>({
18
+ capability: 'wallet',
19
+ hookName,
20
+ providerHint: 'Install a wallet plugin on the client, e.g. `walletSigner()`.',
21
+ }).wallet;
22
+ }
23
+
24
+ // -- State hooks ------------------------------------------------------------
25
+
26
+ /**
27
+ * Subscribes to the wallet connection status from a React component.
28
+ *
29
+ * Wraps `client.wallet.subscribe` / `getState` with `useSyncExternalStore`, re-rendering only when
30
+ * the status changes. Requires a `ClientProvider` whose client has a wallet plugin installed;
31
+ * asserts this at mount with {@link useClientCapability}.
32
+ *
33
+ * @returns The current {@link WalletStatus} (`'pending'` on the server and in React Native).
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * const status = useWalletStatus();
38
+ * if (status === 'pending') return null; // avoid flashing UI before auto-reconnect resolves
39
+ * ```
40
+ *
41
+ * @see {@link useConnectedWallet}
42
+ * @see {@link useWallets}
43
+ */
44
+ export function useWalletStatus(): WalletStatus {
45
+ const wallet = useWalletNamespace('useWalletStatus');
46
+ const getStatus = () => wallet.getState().status;
47
+ return useSyncExternalStore(wallet.subscribe, getStatus, getStatus);
48
+ }
49
+
50
+ /**
51
+ * Subscribes to the active wallet connection from a React component.
52
+ *
53
+ * Wraps `client.wallet.subscribe` / `getState` with `useSyncExternalStore`, re-rendering only when
54
+ * the connection changes (connect, disconnect, or account switch). Requires a `ClientProvider`
55
+ * whose client has a wallet plugin installed; asserts this at mount with {@link useClientCapability}.
56
+ *
57
+ * @returns The active connection — `{ account, signer, wallet }` — or `null` when disconnected.
58
+ * `signer` is `null` for read-only wallets.
59
+ *
60
+ * @example
61
+ * ```tsx
62
+ * const connected = useConnectedWallet();
63
+ * return connected ? <p>{connected.account.address}</p> : <p>Not connected</p>;
64
+ * ```
65
+ *
66
+ * @see {@link useWalletStatus}
67
+ * @see {@link useWallets}
68
+ */
69
+ export function useConnectedWallet(): WalletState['connected'] {
70
+ const wallet = useWalletNamespace('useConnectedWallet');
71
+ const getConnected = () => wallet.getState().connected;
72
+ return useSyncExternalStore(wallet.subscribe, getConnected, getConnected);
73
+ }
74
+
75
+ /**
76
+ * Subscribes to the list of discovered wallets from a React component.
77
+ *
78
+ * Wraps `client.wallet.subscribe` / `getState` with `useSyncExternalStore`, re-rendering only when
79
+ * the discovered-wallet list changes. Requires a `ClientProvider` whose client has a wallet plugin
80
+ * installed; asserts this at mount with {@link useClientCapability}.
81
+ *
82
+ * @returns All discovered wallets matching the client's configured chain and filter (empty on the
83
+ * server and in React Native).
84
+ *
85
+ * @example
86
+ * ```tsx
87
+ * const wallets = useWallets();
88
+ * const connect = useConnect();
89
+ * return wallets.map(w => <button key={w.name} onClick={() => connect.dispatch(w)}>{w.name}</button>);
90
+ * ```
91
+ *
92
+ * @see {@link useWalletStatus}
93
+ * @see {@link useConnectedWallet}
94
+ */
95
+ export function useWallets(): readonly UiWallet[] {
96
+ const wallet = useWalletNamespace('useWallets');
97
+ const getWallets = () => wallet.getState().wallets;
98
+ return useSyncExternalStore(wallet.subscribe, getWallets, getWallets);
99
+ }
100
+
101
+ /**
102
+ * Reports whether the wallet client has finished its initial auto-reconnect "warm-up".
103
+ *
104
+ * Returns `false` while the status is `'pending'` or `'reconnecting'` — the transient states a
105
+ * freshly built client passes through while it silently re-establishes a persisted connection — and
106
+ * `true` once it settles into any stable state (`'connected'`, `'disconnected'`, or a user-initiated
107
+ * `'connecting'` / `'disconnecting'`). This is the declarative, render-time counterpart to the
108
+ * store's imperative `whenReady()`, gating on the exact same set of statuses.
109
+ *
110
+ * Use it to hold back wallet-dependent UI until the connection has settled, so a persisted wallet
111
+ * never flashes "disconnected" before it reconnects. Re-renders only when this readiness boolean
112
+ * flips, not on every status change. Requires a `ClientProvider` whose client has a wallet plugin
113
+ * installed; asserts this at mount with {@link useClientCapability}.
114
+ *
115
+ * @returns `true` once the warm-up has settled, otherwise `false`.
116
+ *
117
+ * @example
118
+ * ```tsx
119
+ * const isReady = useIsWalletReady();
120
+ * return <button disabled={!isReady}>Connect</button>;
121
+ * ```
122
+ *
123
+ * @see {@link WalletReadyGate}
124
+ * @see {@link useWalletStatus}
125
+ */
126
+ export function useIsWalletReady(): boolean {
127
+ const wallet = useWalletNamespace('useIsWalletReady');
128
+ const getIsReady = () => !isWalletWarmingUp(wallet.getState().status);
129
+ return useSyncExternalStore(wallet.subscribe, getIsReady, getIsReady);
130
+ }
131
+
132
+ // -- Action hooks -----------------------------------------------------------
133
+
134
+ /**
135
+ * Connects to a wallet from a React component.
136
+ *
137
+ * Wraps `client.wallet.connect` with {@link useAction}. Requires a `ClientProvider` whose client
138
+ * has a wallet plugin installed; asserts this at mount with {@link useClientCapability}. An
139
+ * in-flight connect is superseded when a newer one is dispatched.
140
+ *
141
+ * `dispatch`/`dispatchAsync` take the {@link UiWallet} to connect to and resolve with the wallet's
142
+ * accounts once connected.
143
+ *
144
+ * @example
145
+ * ```tsx
146
+ * const { dispatch, isRunning } = useConnect();
147
+ * <button disabled={isRunning} onClick={() => dispatch(wallet)}>Connect</button>
148
+ * ```
149
+ *
150
+ * @see {@link useDisconnect}
151
+ * @see {@link useSignIn}
152
+ */
153
+ export function useConnect(): ActionResult<[wallet: UiWallet], readonly UiWalletAccount[]> {
154
+ const wallet = useWalletNamespace('useConnect');
155
+ return useAction((abortSignal, uiWallet: UiWallet) => wallet.connect(uiWallet, { abortSignal }));
156
+ }
157
+
158
+ /**
159
+ * Disconnects a wallet from a React component.
160
+ *
161
+ * Wraps `client.wallet.disconnect` with {@link useAction}. Requires a `ClientProvider` whose client
162
+ * has a wallet plugin installed; asserts this at mount with {@link useClientCapability}.
163
+ *
164
+ * `dispatch`/`dispatchAsync` take an optional {@link UiWallet}. Called with no argument they
165
+ * disconnect the active wallet; passing a non-active, currently authorized wallet deauthorizes that
166
+ * wallet while leaving the active connection in place.
167
+ *
168
+ * @example
169
+ * ```tsx
170
+ * const { dispatch, isRunning } = useDisconnect();
171
+ * // Disconnect the active wallet:
172
+ * <button disabled={isRunning} onClick={() => dispatch()}>Disconnect</button>
173
+ * // Or deauthorize a specific wallet:
174
+ * <button disabled={isRunning} onClick={() => dispatch(wallet)}>Forget this wallet</button>
175
+ * ```
176
+ *
177
+ * @see {@link useConnect}
178
+ */
179
+ export function useDisconnect(): ActionResult<[wallet?: UiWallet], void> {
180
+ const wallet = useWalletNamespace('useDisconnect');
181
+ return useAction((abortSignal, uiWallet?: UiWallet) => wallet.disconnect(uiWallet, { abortSignal }));
182
+ }
183
+
184
+ /**
185
+ * Signs in with a wallet (Sign In With Solana) from a React component.
186
+ *
187
+ * Wraps `client.wallet.signIn` with {@link useAction}. Requires a `ClientProvider` whose client has
188
+ * a wallet plugin installed; asserts this at mount with {@link useClientCapability}. Like
189
+ * {@link useConnect}, this establishes a full connection. An in-flight connect is superseded when a
190
+ * newer one is dispatched.
191
+ *
192
+ * `dispatch`/`dispatchAsync` take the {@link UiWallet} and a `SolanaSignInInput` (pass `{}` for no
193
+ * customization) and resolve with the wallet's sign-in output.
194
+ *
195
+ * @example
196
+ * ```tsx
197
+ * const { dispatch } = useSignIn();
198
+ * dispatch(wallet, { domain: window.location.host });
199
+ * ```
200
+ *
201
+ * @see {@link useConnect}
202
+ */
203
+ export function useSignIn(): ActionResult<[wallet: UiWallet, input: SolanaSignInInput], SolanaSignInOutput> {
204
+ const wallet = useWalletNamespace('useSignIn');
205
+ return useAction((abortSignal, uiWallet: UiWallet, input: SolanaSignInInput) =>
206
+ wallet.signIn(uiWallet, input, { abortSignal }),
207
+ );
208
+ }
209
+
210
+ /**
211
+ * Signs an arbitrary message with the connected account from a React component.
212
+ *
213
+ * Wraps `client.wallet.signMessage` with {@link useAction}. Requires a `ClientProvider` whose
214
+ * client has a wallet plugin installed; asserts this at mount with {@link useClientCapability}.
215
+ * A stale in-flight dispatch is dropped from the hook's state when a newer one starts (though
216
+ * the wallet may still complete the older request in the background).
217
+ *
218
+ * `dispatch`/`dispatchAsync` take the message bytes and resolve with the signature. The bytes are
219
+ * only read, never mutated, so a {@link ReadonlyUint8Array} (e.g. the output of a `@solana/kit`
220
+ * codec) is accepted directly without a cast.
221
+ *
222
+ * @example
223
+ * ```tsx
224
+ * const { dispatch } = useSignMessage();
225
+ * dispatch(new TextEncoder().encode('Hello'));
226
+ * ```
227
+ */
228
+ export function useSignMessage(): ActionResult<[message: ReadonlyUint8Array], SignatureBytes> {
229
+ const wallet = useWalletNamespace('useSignMessage');
230
+ return useAction((abortSignal, message: ReadonlyUint8Array) => wallet.signMessage(message, { abortSignal }));
231
+ }
232
+
233
+ /**
234
+ * Returns a callback that selects a wallet account.
235
+ *
236
+ * Requires a `ClientProvider` whose client has a wallet plugin installed; asserts this at mount
237
+ * with {@link useClientCapability}. Unlike the other action hooks, `selectAccount` is synchronous —
238
+ * this hook returns the bound function directly rather than an `ActionResult`.
239
+ *
240
+ * The account may belong to any currently authorized wallet, not only the active one. Selecting an
241
+ * account from a different authorized wallet switches the active connection to that wallet
242
+ * synchronously (no prompt), leaving the previously active wallet authorized.
243
+ *
244
+ * @returns The `selectAccount` function — call it with a {@link UiWalletAccount} belonging to any
245
+ * currently authorized wallet. It throws if the account's handle can't be resolved to an
246
+ * authorized wallet (or isn't among that wallet's accounts), or that wallet is currently
247
+ * disconnecting.
248
+ *
249
+ * @example
250
+ * ```tsx
251
+ * const selectAccount = useSelectAccount();
252
+ * <button onClick={() => selectAccount(account)}>Use this account</button>
253
+ * ```
254
+ *
255
+ * @see {@link useConnectedWallet}
256
+ */
257
+ export function useSelectAccount(): WalletNamespace['selectAccount'] {
258
+ return useWalletNamespace('useSelectAccount').selectAccount;
259
+ }
260
+
261
+ // -- Components -------------------------------------------------------------
262
+
263
+ /**
264
+ * Props for {@link WalletReadyGate}.
265
+ */
266
+ export type WalletReadyGateProps = Readonly<{
267
+ /** Rendered once the wallet client has finished its initial auto-reconnect warm-up. */
268
+ children: ReactNode;
269
+ /** Rendered while the client is still warming up (`'pending'` / `'reconnecting'`). */
270
+ fallback: ReactNode;
271
+ }>;
272
+
273
+ /**
274
+ * Renders `fallback` while the wallet client is still in its initial auto-reconnect warm-up
275
+ * (`'pending'` / `'reconnecting'`), then reveals `children` once the connection has settled.
276
+ *
277
+ * A declarative wrapper over {@link useIsWalletReady} for the common case of holding back a whole
278
+ * wallet-dependent subtree. Its props are shaped like React's `<Suspense fallback>`, but it is
279
+ * synchronous — it renders `fallback` directly off the reactive status rather than suspending, so it
280
+ * needs no `<Suspense>` ancestor. It lets an app paint its chrome immediately while gating only the
281
+ * wallet-dependent UI, so a persisted wallet never flashes "disconnected" before it silently
282
+ * reconnects. For finer control (disabling a button, showing an inline spinner) read the boolean from
283
+ * {@link useIsWalletReady} directly. To actually suspend a subtree instead, throw the Suspense-safe
284
+ * {@link WalletNamespace.whenReady} promise from your own component.
285
+ *
286
+ * Requires a `ClientProvider` whose client has a wallet plugin installed; asserts this at mount with
287
+ * {@link useClientCapability}.
288
+ *
289
+ * @example
290
+ * ```tsx
291
+ * <WalletReadyGate fallback={<Spinner />}>
292
+ * <WalletDependentApp />
293
+ * </WalletReadyGate>
294
+ * ```
295
+ *
296
+ * @see {@link useIsWalletReady}
297
+ * @see {@link useWalletStatus}
298
+ */
299
+ export function WalletReadyGate({ children, fallback }: WalletReadyGateProps): ReactNode {
300
+ return useIsWalletReady() ? children : fallback;
301
+ }
package/src/status.ts ADDED
@@ -0,0 +1,33 @@
1
+ import type { WalletStatus } from './types';
2
+
3
+ /**
4
+ * Reports whether a {@link WalletStatus} belongs to the initial auto-reconnect
5
+ * "warm-up": `'pending'` (before auto-reconnect has resolved) and
6
+ * `'reconnecting'` (silently re-establishing a persisted connection). During
7
+ * this window a persisted wallet is on its way back, so wallet-dependent UI
8
+ * should usually hold rather than flash a "disconnected" state.
9
+ *
10
+ * User-initiated `'connecting'` / `'disconnecting'` are deliberately excluded —
11
+ * a normal connect or disconnect is not a warm-up and must stay interactive.
12
+ *
13
+ * This is the single source of truth for "warm-up" across the plugin: the
14
+ * store's {@link WalletNamespace.whenReady} waits for the status to leave this set, and the
15
+ * React `useIsWalletReady` hook and `WalletReadyGate` component gate on its
16
+ * negation. Use it directly to gate on warm-up from a non-React consumer.
17
+ *
18
+ * @param status - A status from `client.wallet.getState().status`.
19
+ * @returns `true` while the client is warming up, otherwise `false`.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * client.wallet.subscribe(() => {
24
+ * const { status } = client.wallet.getState();
25
+ * setLoading(isWalletWarmingUp(status));
26
+ * });
27
+ * ```
28
+ *
29
+ * @see {@link WalletNamespace.whenReady} — the imperative (promise-based) counterpart.
30
+ */
31
+ export function isWalletWarmingUp(status: WalletStatus): boolean {
32
+ return status === 'pending' || status === 'reconnecting';
33
+ }