@livo-build/kit 0.1.0 → 0.1.1
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/README.md +44 -4
- package/dist/account/Avatar.d.ts +9 -0
- package/dist/account/Avatar.js +15 -0
- package/dist/account/Identity.d.ts +9 -0
- package/dist/account/Identity.js +11 -0
- package/dist/account/account.css +3 -0
- package/dist/account/gates.d.ts +18 -0
- package/dist/account/gates.js +21 -0
- package/dist/account/index.d.ts +3 -0
- package/dist/account/index.js +3 -0
- package/dist/contracts/index.d.ts +1 -0
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/useContractEvent.d.ts +10 -0
- package/dist/contracts/useContractEvent.js +13 -0
- package/dist/data/Async.d.ts +17 -0
- package/dist/data/Async.js +22 -0
- package/dist/data/index.d.ts +1 -0
- package/dist/data/index.js +1 -0
- package/dist/hooks/index.d.ts +8 -0
- package/dist/hooks/index.js +67 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +10 -0
- package/dist/provider/LivoApp.d.ts +13 -0
- package/dist/provider/LivoApp.js +16 -0
- package/dist/provider/index.d.ts +1 -0
- package/dist/provider/index.js +1 -0
- package/dist/telegram/LinkTelegramButton.d.ts +12 -0
- package/dist/telegram/LinkTelegramButton.js +19 -0
- package/dist/telegram/index.d.ts +3 -0
- package/dist/telegram/index.js +3 -0
- package/dist/telegram/telegram.css +6 -0
- package/dist/telegram/useTelegramLink.d.ts +20 -0
- package/dist/telegram/useTelegramLink.js +114 -0
- package/dist/telegram/useTelegramMiniApp.d.ts +17 -0
- package/dist/telegram/useTelegramMiniApp.js +24 -0
- package/dist/theme/ThemeProvider.d.ts +17 -0
- package/dist/theme/ThemeProvider.js +23 -0
- package/dist/theme/index.d.ts +1 -0
- package/dist/theme/index.js +1 -0
- package/dist/token/ApproveButton.d.ts +12 -0
- package/dist/token/ApproveButton.js +9 -0
- package/dist/token/index.d.ts +5 -0
- package/dist/token/index.js +5 -0
- package/dist/token/useAllowance.d.ts +13 -0
- package/dist/token/useAllowance.js +22 -0
- package/dist/token/useApprove.d.ts +10 -0
- package/dist/token/useApprove.js +14 -0
- package/dist/token/useToken.d.ts +7 -0
- package/dist/token/useToken.js +15 -0
- package/dist/token/useTokenGate.d.ts +17 -0
- package/dist/token/useTokenGate.js +24 -0
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +1 -1
- package/dist/ui/ui.css +11 -0
- package/dist/ui/ui.d.ts +6 -1
- package/dist/ui/ui.js +7 -0
- package/dist/web3/AddressInput.d.ts +10 -0
- package/dist/web3/AddressInput.js +20 -0
- package/dist/web3/ChainSwitcher.d.ts +5 -0
- package/dist/web3/ChainSwitcher.js +11 -0
- package/dist/web3/index.d.ts +2 -0
- package/dist/web3/index.js +2 -0
- package/dist/web3/web3.css +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,16 +102,56 @@ const { data } = useSubgraph({ url, query: GET_ITEMS }); // your indexer's grap
|
|
|
102
102
|
<Card/> <Skeleton width={120}/> <Spinner/> <EmptyState title="Nothing yet"/> <CopyButton value={addr}/>
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
+
## Theme it from one object
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
<KitThemeProvider theme={{ accent: "#6d28d9", radius: 16 }}>
|
|
109
|
+
{/* every kit component beneath inherits the brand */}
|
|
110
|
+
</KitThemeProvider>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Accounts, tokens, approvals
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
<RequireConnection>{/* shown only when connected; else a connect prompt */}</RequireConnection>
|
|
117
|
+
<Connected><Identity address={addr} /></Connected> <Avatar address={addr} />
|
|
118
|
+
|
|
119
|
+
const { symbol, decimals } = useToken(token);
|
|
120
|
+
const { enough } = useAllowance({ token, owner, spender });
|
|
121
|
+
<ApproveButton token={token} spender={spender} amount={amt} /> // approve → toast → done
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Link a wallet to Telegram
|
|
125
|
+
|
|
126
|
+
For apps that have both a Telegram bot and a web instance — verify the same person on both.
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
<LinkTelegramButton /> // Mini App: uses verified initData; web: bot deep-link
|
|
130
|
+
const { status, telegram, link } = useTelegramLink(); // headless
|
|
131
|
+
const mini = useTelegramMiniApp(); // { available, initData, user } when open inside Telegram
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The hook talks to your `api/` (same-origin), which verifies both proofs with
|
|
135
|
+
`@livo-build/runtime` (`verifyTelegramInitData` + a signed-nonce check) and writes
|
|
136
|
+
the binding to D1 (`TelegramLinks`). Endpoints it expects under `endpoint` (default
|
|
137
|
+
`/api/telegram`): `GET /nonce`, `GET /status`, `POST /link` (Mini App), `POST /start`
|
|
138
|
+
(web deep-link), `POST /unlink`. (Livo's `telegram-app` blueprint scaffolds these.)
|
|
139
|
+
|
|
105
140
|
## Exports
|
|
106
141
|
|
|
107
142
|
- **wallet** — `ConnectWallet`, `WalletModal`, `useWalletConnectors`
|
|
143
|
+
- **telegram** — `LinkTelegramButton`, `useTelegramLink`, `useTelegramMiniApp`
|
|
108
144
|
- **provider** — `LivoWeb3Provider`
|
|
109
|
-
- **contracts** — `createLivoContracts`, `useContractValue`, `resolveAddress`
|
|
145
|
+
- **contracts** — `createLivoContracts`, `useContractValue`, `useContractEvent`, `resolveAddress`
|
|
110
146
|
- **tx** — `TxButton`, `useTx`, `decodeRevert`
|
|
111
147
|
- **toast** — `ToastProvider`, `useToast`
|
|
112
|
-
- **data** — `useApi`, `apiClient`, `useSubgraph`
|
|
113
|
-
- **
|
|
114
|
-
- **
|
|
148
|
+
- **data** — `useApi`, `apiClient`, `useSubgraph`, `Async`
|
|
149
|
+
- **token** — `useToken`, `useAllowance`, `useApprove`, `ApproveButton`
|
|
150
|
+
- **account** — `Connected`, `Disconnected`, `RequireConnection`, `useIsConnected`, `Avatar`, `Identity`
|
|
151
|
+
- **web3** — `Address`, `Balance`, `NetworkGuard`, `TokenAmountInput`, `ChainSwitcher`, `AddressInput`
|
|
152
|
+
- **ui** — `Dialog`, `Card`, `Skeleton`, `Spinner`, `EmptyState`, `CopyButton`, `Button`, `Badge`
|
|
153
|
+
- **theme** — `KitThemeProvider`
|
|
154
|
+
- **hooks** — `useCopyToClipboard`, `useLocalStorage`, `useDebounce`, `useInterval`
|
|
115
155
|
- **format** — `shortAddress`, `shortHash`, `formatToken`, `parseToken`, `formatUsd`, `timeAgo`, `avatarGradient`
|
|
116
156
|
|
|
117
157
|
Every component is neutral by default — **style it** (see [STYLING.md](./STYLING.md)).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import "./account.css";
|
|
2
|
+
export interface AvatarProps {
|
|
3
|
+
address: `0x${string}`;
|
|
4
|
+
size?: number;
|
|
5
|
+
/** Resolve + use the ENS avatar when available. Default true. */
|
|
6
|
+
ens?: boolean;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function Avatar({ address, size, ens, className }: AvatarProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import "./account.css";
|
|
3
|
+
import { useEnsName, useEnsAvatar } from "wagmi";
|
|
4
|
+
import { avatarGradient } from "../format";
|
|
5
|
+
import { cx } from "../util";
|
|
6
|
+
// An account avatar: the ENS avatar when set, otherwise a deterministic gradient
|
|
7
|
+
// identicon derived from the address (no image dependency). Neutral.
|
|
8
|
+
export function Avatar({ address, size = 32, ens = true, className }) {
|
|
9
|
+
const { data: name } = useEnsName({ address, query: { enabled: ens } });
|
|
10
|
+
const { data: url } = useEnsAvatar({ name: name ?? undefined, query: { enabled: ens && Boolean(name) } });
|
|
11
|
+
if (url) {
|
|
12
|
+
return _jsx("img", { className: cx("kit-avatar", className), src: url, width: size, height: size, alt: "", style: { borderRadius: "50%" } });
|
|
13
|
+
}
|
|
14
|
+
return (_jsx("span", { className: cx("kit-avatar", className), style: { width: size, height: size, borderRadius: "50%", background: avatarGradient(address), display: "inline-block" }, "aria-hidden": "true" }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import "./account.css";
|
|
2
|
+
export interface IdentityProps {
|
|
3
|
+
address: `0x${string}`;
|
|
4
|
+
size?: number;
|
|
5
|
+
/** Resolve ENS name/avatar. Default true. */
|
|
6
|
+
ens?: boolean;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function Identity({ address, size, ens, className }: IdentityProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import "./account.css";
|
|
3
|
+
import { useEnsName } from "wagmi";
|
|
4
|
+
import { Avatar } from "./Avatar";
|
|
5
|
+
import { shortAddress } from "../format";
|
|
6
|
+
import { cx } from "../util";
|
|
7
|
+
// Avatar + name (ENS or shortened address) in a row. Neutral; style .kit-identity.
|
|
8
|
+
export function Identity({ address, size = 28, ens = true, className }) {
|
|
9
|
+
const { data: name } = useEnsName({ address, query: { enabled: ens } });
|
|
10
|
+
return (_jsxs("span", { className: cx("kit-identity", className), children: [_jsx(Avatar, { address: address, size: size, ens: ens }), _jsx("span", { className: "kit-identity-name", children: name || shortAddress(address) })] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/** Whether a wallet is currently connected. */
|
|
3
|
+
export declare function useIsConnected(): boolean;
|
|
4
|
+
/** Render children only when a wallet is connected (else `fallback`). */
|
|
5
|
+
export declare function Connected({ children, fallback }: {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
fallback?: ReactNode;
|
|
8
|
+
}): import("react").JSX.Element;
|
|
9
|
+
/** Render children only when NO wallet is connected. */
|
|
10
|
+
export declare function Disconnected({ children }: {
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
}): import("react").JSX.Element | null;
|
|
13
|
+
/** Gate UI behind a connection: shows the children when connected, otherwise a
|
|
14
|
+
* connect prompt (the kit's <ConnectWallet/> by default — pass `connect` to override). */
|
|
15
|
+
export declare function RequireConnection({ children, connect }: {
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
connect?: ReactNode;
|
|
18
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import {} from "react";
|
|
3
|
+
import { useConnection } from "wagmi";
|
|
4
|
+
import { ConnectWallet } from "../wallet";
|
|
5
|
+
/** Whether a wallet is currently connected. */
|
|
6
|
+
export function useIsConnected() {
|
|
7
|
+
return useConnection().isConnected;
|
|
8
|
+
}
|
|
9
|
+
/** Render children only when a wallet is connected (else `fallback`). */
|
|
10
|
+
export function Connected({ children, fallback }) {
|
|
11
|
+
return useIsConnected() ? _jsx(_Fragment, { children: children }) : _jsx(_Fragment, { children: fallback ?? null });
|
|
12
|
+
}
|
|
13
|
+
/** Render children only when NO wallet is connected. */
|
|
14
|
+
export function Disconnected({ children }) {
|
|
15
|
+
return useIsConnected() ? null : _jsx(_Fragment, { children: children });
|
|
16
|
+
}
|
|
17
|
+
/** Gate UI behind a connection: shows the children when connected, otherwise a
|
|
18
|
+
* connect prompt (the kit's <ConnectWallet/> by default — pass `connect` to override). */
|
|
19
|
+
export function RequireConnection({ children, connect }) {
|
|
20
|
+
return useIsConnected() ? _jsx(_Fragment, { children: children }) : _jsx(_Fragment, { children: connect ?? _jsx(ConnectWallet, {}) });
|
|
21
|
+
}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { useContractValue, type UseContractValueParams } from "./useContractValue";
|
|
2
|
+
export { useContractEvent, type UseContractEventParams } from "./useContractEvent";
|
|
2
3
|
export { createLivoContracts, resolveAddress, type LivoBindings, type LivoContracts, type ContractRef, } from "./createLivoContracts";
|
package/dist/contracts/index.js
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Abi } from "viem";
|
|
2
|
+
export interface UseContractEventParams {
|
|
3
|
+
address?: `0x${string}`;
|
|
4
|
+
abi: Abi;
|
|
5
|
+
eventName: string;
|
|
6
|
+
onLogs: (logs: readonly unknown[]) => void;
|
|
7
|
+
chainId?: number;
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function useContractEvent({ address, abi, eventName, onLogs, chainId, enabled }: UseContractEventParams): void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useWatchContractEvent } from "wagmi";
|
|
2
|
+
// Subscribe to a contract event (live). A thin wrapper over useWatchContractEvent —
|
|
3
|
+
// pair with createLivoContracts' useContract(name) to get { address, abi }.
|
|
4
|
+
export function useContractEvent({ address, abi, eventName, onLogs, chainId, enabled }) {
|
|
5
|
+
useWatchContractEvent({
|
|
6
|
+
address,
|
|
7
|
+
abi,
|
|
8
|
+
eventName,
|
|
9
|
+
chainId,
|
|
10
|
+
enabled: (enabled ?? true) && Boolean(address),
|
|
11
|
+
onLogs: (logs) => onLogs(logs),
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export interface AsyncQuery<T> {
|
|
3
|
+
data: T | undefined;
|
|
4
|
+
error: Error | null;
|
|
5
|
+
isLoading: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface AsyncProps<T> {
|
|
8
|
+
query: AsyncQuery<T>;
|
|
9
|
+
/** Render the loaded data. */
|
|
10
|
+
children: (data: T) => ReactNode;
|
|
11
|
+
loading?: ReactNode;
|
|
12
|
+
error?: (error: Error) => ReactNode;
|
|
13
|
+
/** Shown when data is "empty" (default: [] or null/undefined). */
|
|
14
|
+
empty?: ReactNode;
|
|
15
|
+
isEmpty?: (data: T) => boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function Async<T>({ query, children, loading, error, empty, isEmpty }: AsyncProps<T>): import("react").JSX.Element;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import {} from "react";
|
|
3
|
+
import { Spinner } from "../ui";
|
|
4
|
+
// Render the loading / error / empty / data states of ANY kit data hook in one
|
|
5
|
+
// place — `<Async query={useApi("/items")}>{(items) => …}</Async>`. Neutral
|
|
6
|
+
// defaults (spinner, plain error); pass your own for each state.
|
|
7
|
+
export function Async({ query, children, loading, error, empty, isEmpty }) {
|
|
8
|
+
if (query.error) {
|
|
9
|
+
return _jsx(_Fragment, { children: error ? error(query.error) : _jsx("span", { className: "kit-async-error", children: query.error.message }) });
|
|
10
|
+
}
|
|
11
|
+
if (query.isLoading || query.data === undefined) {
|
|
12
|
+
return _jsx(_Fragment, { children: loading ?? _jsx(Spinner, {}) });
|
|
13
|
+
}
|
|
14
|
+
const emptyVal = isEmpty
|
|
15
|
+
? isEmpty(query.data)
|
|
16
|
+
: Array.isArray(query.data)
|
|
17
|
+
? query.data.length === 0
|
|
18
|
+
: query.data === null;
|
|
19
|
+
if (emptyVal && empty !== undefined)
|
|
20
|
+
return _jsx(_Fragment, { children: empty });
|
|
21
|
+
return _jsx(_Fragment, { children: children(query.data) });
|
|
22
|
+
}
|
package/dist/data/index.d.ts
CHANGED
package/dist/data/index.js
CHANGED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Copy text to the clipboard; returns [copied, copy]. `copied` flips true for ~1.2s. */
|
|
2
|
+
export declare function useCopyToClipboard(): [boolean, (text: string) => void];
|
|
3
|
+
/** State persisted to localStorage (SSR-safe; falls back to in-memory). */
|
|
4
|
+
export declare function useLocalStorage<T>(key: string, initial: T): [T, (value: T) => void];
|
|
5
|
+
/** Debounce a changing value by `ms` milliseconds. */
|
|
6
|
+
export declare function useDebounce<T>(value: T, ms?: number): T;
|
|
7
|
+
/** Run a callback every `ms` milliseconds. Pass `ms = null` to pause. */
|
|
8
|
+
export declare function useInterval(callback: () => void, ms: number | null): void;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
/** Copy text to the clipboard; returns [copied, copy]. `copied` flips true for ~1.2s. */
|
|
3
|
+
export function useCopyToClipboard() {
|
|
4
|
+
const [copied, setCopied] = useState(false);
|
|
5
|
+
const timer = useRef(null);
|
|
6
|
+
const copy = useCallback((text) => {
|
|
7
|
+
try {
|
|
8
|
+
navigator.clipboard.writeText(text);
|
|
9
|
+
setCopied(true);
|
|
10
|
+
if (timer.current)
|
|
11
|
+
clearTimeout(timer.current);
|
|
12
|
+
timer.current = setTimeout(() => setCopied(false), 1200);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
/* clipboard unavailable */
|
|
16
|
+
}
|
|
17
|
+
}, []);
|
|
18
|
+
useEffect(() => () => { if (timer.current)
|
|
19
|
+
clearTimeout(timer.current); }, []);
|
|
20
|
+
return [copied, copy];
|
|
21
|
+
}
|
|
22
|
+
/** State persisted to localStorage (SSR-safe; falls back to in-memory). */
|
|
23
|
+
export function useLocalStorage(key, initial) {
|
|
24
|
+
const [value, setValue] = useState(() => {
|
|
25
|
+
if (typeof window === "undefined")
|
|
26
|
+
return initial;
|
|
27
|
+
try {
|
|
28
|
+
const raw = window.localStorage.getItem(key);
|
|
29
|
+
return raw ? JSON.parse(raw) : initial;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return initial;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
const set = useCallback((next) => {
|
|
36
|
+
setValue(next);
|
|
37
|
+
try {
|
|
38
|
+
window.localStorage.setItem(key, JSON.stringify(next));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
/* storage unavailable / quota */
|
|
42
|
+
}
|
|
43
|
+
}, [key]);
|
|
44
|
+
return [value, set];
|
|
45
|
+
}
|
|
46
|
+
/** Debounce a changing value by `ms` milliseconds. */
|
|
47
|
+
export function useDebounce(value, ms = 300) {
|
|
48
|
+
const [debounced, setDebounced] = useState(value);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const t = setTimeout(() => setDebounced(value), ms);
|
|
51
|
+
return () => clearTimeout(t);
|
|
52
|
+
}, [value, ms]);
|
|
53
|
+
return debounced;
|
|
54
|
+
}
|
|
55
|
+
/** Run a callback every `ms` milliseconds. Pass `ms = null` to pause. */
|
|
56
|
+
export function useInterval(callback, ms) {
|
|
57
|
+
const saved = useRef(callback);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
saved.current = callback;
|
|
60
|
+
}, [callback]);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (ms == null)
|
|
63
|
+
return;
|
|
64
|
+
const id = setInterval(() => saved.current(), ms);
|
|
65
|
+
return () => clearInterval(id);
|
|
66
|
+
}, [ms]);
|
|
67
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,5 +5,10 @@ export * from "./tx";
|
|
|
5
5
|
export * from "./toast";
|
|
6
6
|
export * from "./data";
|
|
7
7
|
export * from "./web3";
|
|
8
|
+
export * from "./token";
|
|
9
|
+
export * from "./account";
|
|
8
10
|
export * from "./ui";
|
|
11
|
+
export * from "./telegram";
|
|
12
|
+
export * from "./theme";
|
|
13
|
+
export * from "./hooks";
|
|
9
14
|
export * from "./format";
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
// toast/ — ToastProvider, useToast
|
|
10
10
|
// data/ — useApi (same-origin /api/*), useSubgraph (indexer)
|
|
11
11
|
// web3/ — Address, Balance, NetworkGuard, TokenAmountInput
|
|
12
|
+
// token/ — useToken, useAllowance, useApprove, ApproveButton (ERC-20 + approvals)
|
|
13
|
+
// account/ — Connected, Disconnected, RequireConnection, useIsConnected, Avatar, Identity
|
|
12
14
|
// ui/ — Dialog, Card, Skeleton, Spinner, EmptyState, CopyButton
|
|
15
|
+
// telegram/ — useTelegramMiniApp, useTelegramLink, LinkTelegramButton (link a wallet ↔ Telegram)
|
|
16
|
+
// theme/ — KitThemeProvider (restyle the whole kit from one theme object)
|
|
17
|
+
// hooks/ — useCopyToClipboard, useLocalStorage, useDebounce, useInterval
|
|
13
18
|
// format — shortAddress, formatToken, parseToken, formatUsd, timeAgo, …
|
|
14
19
|
export * from "./wallet";
|
|
15
20
|
export * from "./provider";
|
|
@@ -18,5 +23,10 @@ export * from "./tx";
|
|
|
18
23
|
export * from "./toast";
|
|
19
24
|
export * from "./data";
|
|
20
25
|
export * from "./web3";
|
|
26
|
+
export * from "./token";
|
|
27
|
+
export * from "./account";
|
|
21
28
|
export * from "./ui";
|
|
29
|
+
export * from "./telegram";
|
|
30
|
+
export * from "./theme";
|
|
31
|
+
export * from "./hooks";
|
|
22
32
|
export * from "./format";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { QueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { type Config } from "wagmi";
|
|
4
|
+
import { type KitTheme } from "../theme";
|
|
5
|
+
export interface LivoAppProps {
|
|
6
|
+
/** Your wagmi config (chains + connectors). */
|
|
7
|
+
config: Config;
|
|
8
|
+
/** Optional theme tokens applied to every kit component (accent, radius, …). */
|
|
9
|
+
theme?: KitTheme;
|
|
10
|
+
queryClient?: QueryClient;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
export declare function LivoApp({ config, theme, queryClient, children }: LivoAppProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import {} from "react";
|
|
3
|
+
import { QueryClient } from "@tanstack/react-query";
|
|
4
|
+
import {} from "wagmi";
|
|
5
|
+
import { LivoWeb3Provider } from "./Web3Provider";
|
|
6
|
+
import { ToastProvider } from "../toast";
|
|
7
|
+
import { KitThemeProvider } from "../theme";
|
|
8
|
+
// One wrapper for a Livo web3 app: wagmi + react-query (LivoWeb3Provider), toasts
|
|
9
|
+
// (ToastProvider for TxButton et al.), and optional theming — so you wrap once
|
|
10
|
+
// instead of nesting three providers. Pass your wagmi `config`; everything else is
|
|
11
|
+
// optional.
|
|
12
|
+
export function LivoApp({ config, theme, queryClient, children }) {
|
|
13
|
+
const withToasts = _jsx(ToastProvider, { children: children });
|
|
14
|
+
const themed = theme ? _jsx(KitThemeProvider, { theme: theme, children: withToasts }) : withToasts;
|
|
15
|
+
return (_jsx(LivoWeb3Provider, { config: config, queryClient: queryClient, children: themed }));
|
|
16
|
+
}
|
package/dist/provider/index.d.ts
CHANGED
package/dist/provider/index.js
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import "./telegram.css";
|
|
2
|
+
import { type UseTelegramLinkOptions } from "./useTelegramLink";
|
|
3
|
+
export interface LinkTelegramButtonProps extends UseTelegramLinkOptions {
|
|
4
|
+
className?: string;
|
|
5
|
+
labels?: {
|
|
6
|
+
link?: string;
|
|
7
|
+
linking?: string;
|
|
8
|
+
open?: string;
|
|
9
|
+
linked?: (username?: string) => string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export declare function LinkTelegramButton({ className, labels, ...opts }: LinkTelegramButtonProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import "./telegram.css";
|
|
3
|
+
import { useTelegramLink } from "./useTelegramLink";
|
|
4
|
+
import { cx } from "../util";
|
|
5
|
+
// One button for the whole Telegram-link lifecycle: "Link Telegram" → (Mini App)
|
|
6
|
+
// done, or (open web) "Open Telegram to finish" deep link → linked chip. Neutral;
|
|
7
|
+
// style .kit-tg / [data-state]. Build your own on useTelegramLink for full control.
|
|
8
|
+
export function LinkTelegramButton({ className, labels, ...opts }) {
|
|
9
|
+
const { status, telegram, deepLink, link, unlink } = useTelegramLink(opts);
|
|
10
|
+
if (status === "linked") {
|
|
11
|
+
const who = telegram?.username ? `@${telegram.username}` : telegram?.id ? `#${telegram.id}` : "";
|
|
12
|
+
return (_jsx("button", { className: cx("kit-tg", className), "data-state": "linked", onClick: () => unlink(), children: labels?.linked ? labels.linked(telegram?.username) : `Telegram ${who} ✓` }));
|
|
13
|
+
}
|
|
14
|
+
if (status === "linking" && deepLink) {
|
|
15
|
+
return (_jsx("a", { className: cx("kit-tg", className), "data-state": "open", href: deepLink, target: "_blank", rel: "noreferrer", children: labels?.open ?? "Open Telegram to finish" }));
|
|
16
|
+
}
|
|
17
|
+
const busy = status === "linking" || status === "loading";
|
|
18
|
+
return (_jsx("button", { className: cx("kit-tg", className), "data-state": status, disabled: busy, onClick: () => link(), children: status === "linking" ? (labels?.linking ?? "Linking…") : (labels?.link ?? "Link Telegram") }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { useTelegramMiniApp, type TelegramMiniApp } from "./useTelegramMiniApp";
|
|
2
|
+
export { useTelegramLink, type TelegramLinkState, type LinkStatus, type LinkedTelegram, type UseTelegramLinkOptions, } from "./useTelegramLink";
|
|
3
|
+
export { LinkTelegramButton, type LinkTelegramButtonProps } from "./LinkTelegramButton";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
.kit-tg{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:10px 16px;border:0;border-radius:11px;font:inherit;font-weight:600;font-size:14px;cursor:pointer;text-decoration:none;transition:opacity .12s,transform .08s;background:var(--kit-accent,#229ED9);color:var(--kit-accent-fg,#fff)}
|
|
2
|
+
.kit-tg:hover{opacity:.9}
|
|
3
|
+
.kit-tg:active{transform:translateY(1px)}
|
|
4
|
+
.kit-tg:disabled{opacity:.6;cursor:default}
|
|
5
|
+
.kit-tg[data-state="linked"]{background:var(--kit-row,#f3f4f6);color:var(--kit-fg,#111827)}
|
|
6
|
+
@media (prefers-color-scheme:dark){.kit-tg[data-state="linked"]{--kit-row:#27272c;--kit-fg:#f5f5f7}}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type LinkStatus = "loading" | "unlinked" | "linking" | "linked" | "error";
|
|
2
|
+
export interface LinkedTelegram {
|
|
3
|
+
id: number;
|
|
4
|
+
username?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface TelegramLinkState {
|
|
7
|
+
status: LinkStatus;
|
|
8
|
+
telegram?: LinkedTelegram;
|
|
9
|
+
/** Open-web flow: a t.me deep link to finish linking in the bot. */
|
|
10
|
+
deepLink?: string;
|
|
11
|
+
link: () => Promise<void>;
|
|
12
|
+
unlink: () => Promise<void>;
|
|
13
|
+
refresh: () => void;
|
|
14
|
+
error?: Error;
|
|
15
|
+
}
|
|
16
|
+
export interface UseTelegramLinkOptions {
|
|
17
|
+
/** Base path for the link API (default "/api/telegram"). */
|
|
18
|
+
endpoint?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function useTelegramLink(options?: UseTelegramLinkOptions): TelegramLinkState;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { useConnection, useSignMessage } from "wagmi";
|
|
3
|
+
import { useTelegramMiniApp } from "./useTelegramMiniApp";
|
|
4
|
+
// Link the connected wallet to the user's Telegram identity. Inside a Telegram Mini
|
|
5
|
+
// App it uses the verified initData; on a normal website it falls back to a bot
|
|
6
|
+
// deep-link handshake (poll until the bot completes it). The server (your api/)
|
|
7
|
+
// verifies both the Telegram identity and the wallet signature before binding.
|
|
8
|
+
export function useTelegramLink(options = {}) {
|
|
9
|
+
const base = options.endpoint ?? "/api/telegram";
|
|
10
|
+
const { address } = useConnection();
|
|
11
|
+
const { signMessageAsync } = useSignMessage();
|
|
12
|
+
const mini = useTelegramMiniApp();
|
|
13
|
+
const [status, setStatus] = useState("loading");
|
|
14
|
+
const [telegram, setTelegram] = useState();
|
|
15
|
+
const [deepLink, setDeepLink] = useState();
|
|
16
|
+
const [error, setError] = useState();
|
|
17
|
+
const pollRef = useRef(null);
|
|
18
|
+
const stopPoll = () => {
|
|
19
|
+
if (pollRef.current) {
|
|
20
|
+
clearInterval(pollRef.current);
|
|
21
|
+
pollRef.current = null;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const refresh = useCallback(async () => {
|
|
25
|
+
if (!address) {
|
|
26
|
+
setStatus("unlinked");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const r = await fetch(`${base}/status?address=${address}`);
|
|
31
|
+
const j = (await r.json());
|
|
32
|
+
if (j.linked) {
|
|
33
|
+
setStatus("linked");
|
|
34
|
+
setTelegram(j.telegram);
|
|
35
|
+
stopPoll();
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
setStatus((s) => (s === "linking" ? s : "unlinked"));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
setError(e);
|
|
43
|
+
setStatus("error");
|
|
44
|
+
}
|
|
45
|
+
}, [address, base]);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
void refresh();
|
|
48
|
+
return stopPoll;
|
|
49
|
+
}, [refresh]);
|
|
50
|
+
const sign = useCallback(async () => {
|
|
51
|
+
const nonceRes = await fetch(`${base}/nonce?address=${address}`);
|
|
52
|
+
const { nonce, message } = (await nonceRes.json());
|
|
53
|
+
const msg = message ?? `Link my Telegram to ${address}\nnonce: ${nonce}`;
|
|
54
|
+
const signature = await signMessageAsync({ message: msg });
|
|
55
|
+
return { nonce, signature };
|
|
56
|
+
}, [address, base, signMessageAsync]);
|
|
57
|
+
const link = useCallback(async () => {
|
|
58
|
+
if (!address)
|
|
59
|
+
return;
|
|
60
|
+
setStatus("linking");
|
|
61
|
+
setError(undefined);
|
|
62
|
+
try {
|
|
63
|
+
const { nonce, signature } = await sign();
|
|
64
|
+
if (mini.available) {
|
|
65
|
+
const res = await fetch(`${base}/link`, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: { "content-type": "application/json" },
|
|
68
|
+
body: JSON.stringify({ initData: mini.initData, address, signature, nonce }),
|
|
69
|
+
});
|
|
70
|
+
const j = (await res.json());
|
|
71
|
+
if (!res.ok || !j.linked)
|
|
72
|
+
throw new Error(j.error ?? "Link failed");
|
|
73
|
+
setStatus("linked");
|
|
74
|
+
setTelegram(j.telegram);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const res = await fetch(`${base}/start`, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: { "content-type": "application/json" },
|
|
80
|
+
body: JSON.stringify({ address, signature, nonce }),
|
|
81
|
+
});
|
|
82
|
+
const j = (await res.json());
|
|
83
|
+
if (!res.ok || !j.deepLink)
|
|
84
|
+
throw new Error(j.error ?? "Link failed");
|
|
85
|
+
setDeepLink(j.deepLink);
|
|
86
|
+
stopPoll();
|
|
87
|
+
pollRef.current = setInterval(() => void refresh(), 3000);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
setError(e);
|
|
92
|
+
setStatus("error");
|
|
93
|
+
}
|
|
94
|
+
}, [address, base, mini, sign, refresh]);
|
|
95
|
+
const unlink = useCallback(async () => {
|
|
96
|
+
if (!address)
|
|
97
|
+
return;
|
|
98
|
+
try {
|
|
99
|
+
const { nonce, signature } = await sign();
|
|
100
|
+
await fetch(`${base}/unlink`, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: { "content-type": "application/json" },
|
|
103
|
+
body: JSON.stringify({ address, signature, nonce }),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
/* ignore */
|
|
108
|
+
}
|
|
109
|
+
setStatus("unlinked");
|
|
110
|
+
setTelegram(undefined);
|
|
111
|
+
setDeepLink(undefined);
|
|
112
|
+
}, [address, base, sign]);
|
|
113
|
+
return { status, telegram, deepLink, link, unlink, refresh: () => void refresh(), error };
|
|
114
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface TelegramMiniApp {
|
|
2
|
+
/** True when the page is running inside Telegram (initData present). */
|
|
3
|
+
available: boolean;
|
|
4
|
+
/** Raw signed initData — send to the server to verify the user. */
|
|
5
|
+
initData: string;
|
|
6
|
+
/** Client-side (UNVERIFIED) user, for display only — verify server-side. */
|
|
7
|
+
user?: {
|
|
8
|
+
id: number;
|
|
9
|
+
username?: string;
|
|
10
|
+
firstName?: string;
|
|
11
|
+
photoUrl?: string;
|
|
12
|
+
};
|
|
13
|
+
colorScheme?: "light" | "dark";
|
|
14
|
+
ready: () => void;
|
|
15
|
+
expand: () => void;
|
|
16
|
+
}
|
|
17
|
+
export declare function useTelegramMiniApp(): TelegramMiniApp;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
function getWebApp() {
|
|
3
|
+
if (typeof window === "undefined")
|
|
4
|
+
return undefined;
|
|
5
|
+
return window.Telegram?.WebApp;
|
|
6
|
+
}
|
|
7
|
+
// Read the Telegram Mini App context (window.Telegram.WebApp), if the page is open
|
|
8
|
+
// inside Telegram. Requires the Telegram WebApp SDK script
|
|
9
|
+
// (https://telegram.org/js/telegram-web-app.js) in your <head>.
|
|
10
|
+
export function useTelegramMiniApp() {
|
|
11
|
+
const wa = getWebApp();
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
wa?.ready();
|
|
14
|
+
}, [wa]);
|
|
15
|
+
const u = wa?.initDataUnsafe?.user;
|
|
16
|
+
return {
|
|
17
|
+
available: Boolean(wa?.initData),
|
|
18
|
+
initData: wa?.initData ?? "",
|
|
19
|
+
user: u ? { id: u.id, username: u.username, firstName: u.first_name, photoUrl: u.photo_url } : undefined,
|
|
20
|
+
colorScheme: wa?.colorScheme,
|
|
21
|
+
ready: () => wa?.ready(),
|
|
22
|
+
expand: () => wa?.expand(),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ReactNode, type CSSProperties } from "react";
|
|
2
|
+
export interface KitTheme {
|
|
3
|
+
accent?: string;
|
|
4
|
+
accentFg?: string;
|
|
5
|
+
radius?: number | string;
|
|
6
|
+
bg?: string;
|
|
7
|
+
fg?: string;
|
|
8
|
+
muted?: string;
|
|
9
|
+
border?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface KitThemeProviderProps {
|
|
12
|
+
theme: KitTheme;
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
className?: string;
|
|
15
|
+
style?: CSSProperties;
|
|
16
|
+
}
|
|
17
|
+
export declare function KitThemeProvider({ theme, children, className, style }: KitThemeProviderProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import {} from "react";
|
|
3
|
+
const VARS = {
|
|
4
|
+
accent: ["--wm-accent", "--kit-accent", "--kit-t-accent"],
|
|
5
|
+
accentFg: ["--wm-accent-fg", "--kit-accent-fg"],
|
|
6
|
+
radius: ["--wm-radius", "--kit-radius"],
|
|
7
|
+
bg: ["--wm-bg", "--kit-bg", "--kit-t-bg"],
|
|
8
|
+
fg: ["--wm-fg", "--kit-fg", "--kit-t-fg"],
|
|
9
|
+
muted: ["--wm-muted", "--kit-muted", "--kit-t-muted"],
|
|
10
|
+
border: ["--wm-border", "--kit-border", "--kit-t-border"],
|
|
11
|
+
};
|
|
12
|
+
export function KitThemeProvider({ theme, children, className, style }) {
|
|
13
|
+
const vars = {};
|
|
14
|
+
Object.keys(theme).forEach((k) => {
|
|
15
|
+
const v = theme[k];
|
|
16
|
+
if (v == null)
|
|
17
|
+
return;
|
|
18
|
+
const value = typeof v === "number" ? `${v}px` : String(v);
|
|
19
|
+
for (const cssVar of VARS[k])
|
|
20
|
+
vars[cssVar] = value;
|
|
21
|
+
});
|
|
22
|
+
return (_jsx("div", { className: className, style: { ...vars, ...style }, children: children }));
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { KitThemeProvider, type KitTheme, type KitThemeProviderProps } from "./ThemeProvider";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { KitThemeProvider } from "./ThemeProvider";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export interface ApproveButtonProps {
|
|
3
|
+
token: `0x${string}`;
|
|
4
|
+
spender: `0x${string}`;
|
|
5
|
+
/** Amount to approve (base units). Omit for unlimited. */
|
|
6
|
+
amount?: bigint;
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
toast?: boolean;
|
|
10
|
+
onSuccess?: (hash: `0x${string}`) => void;
|
|
11
|
+
}
|
|
12
|
+
export declare function ApproveButton({ token, spender, amount, children, className, toast, onSuccess }: ApproveButtonProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import {} from "react";
|
|
3
|
+
import { erc20Abi, maxUint256 } from "viem";
|
|
4
|
+
import { TxButton } from "../tx";
|
|
5
|
+
// A TxButton wired for ERC-20 approve — pending/confirming states + toast + explorer
|
|
6
|
+
// link, same as any other write.
|
|
7
|
+
export function ApproveButton({ token, spender, amount, children, className, toast, onSuccess }) {
|
|
8
|
+
return (_jsx(TxButton, { address: token, abi: erc20Abi, functionName: "approve", args: [spender, amount ?? maxUint256], className: className, toast: toast, onSuccess: onSuccess, children: children ?? "Approve" }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { useToken, type TokenInfo } from "./useToken";
|
|
2
|
+
export { useAllowance, type AllowanceResult, type UseAllowanceParams } from "./useAllowance";
|
|
3
|
+
export { useApprove, type UseApproveParams } from "./useApprove";
|
|
4
|
+
export { ApproveButton, type ApproveButtonProps } from "./ApproveButton";
|
|
5
|
+
export { useTokenGate, type UseTokenGateParams, type TokenGateResult } from "./useTokenGate";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface AllowanceResult {
|
|
2
|
+
data: bigint | undefined;
|
|
3
|
+
/** Whether the allowance covers `amount` (false until loaded, or if `amount` omitted). */
|
|
4
|
+
enough: (amount: bigint) => boolean;
|
|
5
|
+
isLoading: boolean;
|
|
6
|
+
refetch: () => void;
|
|
7
|
+
}
|
|
8
|
+
export interface UseAllowanceParams {
|
|
9
|
+
token?: `0x${string}`;
|
|
10
|
+
owner?: `0x${string}`;
|
|
11
|
+
spender?: `0x${string}`;
|
|
12
|
+
}
|
|
13
|
+
export declare function useAllowance({ token, owner, spender }: UseAllowanceParams): AllowanceResult;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useReadContract } from "wagmi";
|
|
2
|
+
import { erc20Abi } from "viem";
|
|
3
|
+
// The ERC-20 allowance owner has granted spender. Pair with useApprove — the
|
|
4
|
+
// classic approve→spend gate.
|
|
5
|
+
export function useAllowance({ token, owner, spender }) {
|
|
6
|
+
const r = useReadContract({
|
|
7
|
+
address: token,
|
|
8
|
+
abi: erc20Abi,
|
|
9
|
+
functionName: "allowance",
|
|
10
|
+
args: owner && spender ? [owner, spender] : undefined,
|
|
11
|
+
query: { enabled: Boolean(token && owner && spender) },
|
|
12
|
+
});
|
|
13
|
+
const data = r.data;
|
|
14
|
+
return {
|
|
15
|
+
data,
|
|
16
|
+
enough: (amount) => data !== undefined && data >= amount,
|
|
17
|
+
isLoading: r.isLoading,
|
|
18
|
+
refetch: () => {
|
|
19
|
+
void r.refetch();
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type UseTxResult } from "../tx/useTx";
|
|
2
|
+
export interface UseApproveParams {
|
|
3
|
+
token: `0x${string}`;
|
|
4
|
+
spender: `0x${string}`;
|
|
5
|
+
/** Amount to approve (base units). Omit for an unlimited (maxUint256) approval. */
|
|
6
|
+
amount?: bigint;
|
|
7
|
+
onSuccess?: () => void;
|
|
8
|
+
onError?: (error: Error) => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function useApprove({ token, spender, amount, onSuccess, onError }: UseApproveParams): UseTxResult;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { erc20Abi, maxUint256 } from "viem";
|
|
2
|
+
import { useTx } from "../tx/useTx";
|
|
3
|
+
// Approve an ERC-20 spender — the whole approve tx lifecycle via useTx. Read the
|
|
4
|
+
// current allowance with useAllowance to decide whether you even need this.
|
|
5
|
+
export function useApprove({ token, spender, amount, onSuccess, onError }) {
|
|
6
|
+
return useTx({
|
|
7
|
+
address: token,
|
|
8
|
+
abi: erc20Abi,
|
|
9
|
+
functionName: "approve",
|
|
10
|
+
args: [spender, amount ?? maxUint256],
|
|
11
|
+
onSuccess: onSuccess ? () => onSuccess() : undefined,
|
|
12
|
+
onError,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useReadContract } from "wagmi";
|
|
2
|
+
import { erc20Abi } from "viem";
|
|
3
|
+
// ERC-20 metadata (name / symbol / decimals) for a token address.
|
|
4
|
+
export function useToken(token) {
|
|
5
|
+
const enabled = Boolean(token);
|
|
6
|
+
const name = useReadContract({ address: token, abi: erc20Abi, functionName: "name", query: { enabled } });
|
|
7
|
+
const symbol = useReadContract({ address: token, abi: erc20Abi, functionName: "symbol", query: { enabled } });
|
|
8
|
+
const decimals = useReadContract({ address: token, abi: erc20Abi, functionName: "decimals", query: { enabled } });
|
|
9
|
+
return {
|
|
10
|
+
name: name.data,
|
|
11
|
+
symbol: symbol.data,
|
|
12
|
+
decimals: decimals.data,
|
|
13
|
+
isLoading: name.isLoading || symbol.isLoading || decimals.isLoading,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface UseTokenGateParams {
|
|
2
|
+
/** ERC-20 / ERC-721 token address that gates access. */
|
|
3
|
+
token: `0x${string}`;
|
|
4
|
+
/** Minimum balance required (base units for ERC-20; count for ERC-721). */
|
|
5
|
+
threshold: bigint;
|
|
6
|
+
/** Account to check (defaults to the connected wallet). */
|
|
7
|
+
address?: `0x${string}`;
|
|
8
|
+
}
|
|
9
|
+
export interface TokenGateResult {
|
|
10
|
+
/** Whether the wallet currently meets the gate. */
|
|
11
|
+
eligible: boolean;
|
|
12
|
+
balance: bigint | undefined;
|
|
13
|
+
/** How much more is needed to qualify (0n when eligible). */
|
|
14
|
+
shortBy: bigint;
|
|
15
|
+
isLoading: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function useTokenGate({ token, threshold, address }: UseTokenGateParams): TokenGateResult;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useReadContract, useConnection } from "wagmi";
|
|
2
|
+
import { erc20Abi } from "viem";
|
|
3
|
+
// Does a wallet hold enough of a token to pass a gate? For showing eligibility on
|
|
4
|
+
// the link/join page (the bot/keeper enforces it server-side with meetsGate). Works
|
|
5
|
+
// for ERC-20 (balance) and ERC-721 (count) via balanceOf.
|
|
6
|
+
export function useTokenGate({ token, threshold, address }) {
|
|
7
|
+
const { address: connected } = useConnection();
|
|
8
|
+
const owner = address ?? connected;
|
|
9
|
+
const r = useReadContract({
|
|
10
|
+
address: token,
|
|
11
|
+
abi: erc20Abi,
|
|
12
|
+
functionName: "balanceOf",
|
|
13
|
+
args: owner ? [owner] : undefined,
|
|
14
|
+
query: { enabled: Boolean(token && owner) },
|
|
15
|
+
});
|
|
16
|
+
const balance = r.data;
|
|
17
|
+
const eligible = balance !== undefined && balance >= threshold;
|
|
18
|
+
return {
|
|
19
|
+
eligible,
|
|
20
|
+
balance,
|
|
21
|
+
shortBy: balance !== undefined && balance < threshold ? threshold - balance : 0n,
|
|
22
|
+
isLoading: r.isLoading,
|
|
23
|
+
};
|
|
24
|
+
}
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { Dialog, Card, Skeleton, Spinner, EmptyState, CopyButton, type DialogProps, type SkeletonProps, type EmptyStateProps, type CopyButtonProps, } from "./ui";
|
|
1
|
+
export { Dialog, Card, Skeleton, Spinner, EmptyState, CopyButton, Button, Badge, type DialogProps, type SkeletonProps, type EmptyStateProps, type CopyButtonProps, type ButtonProps, } from "./ui";
|
package/dist/ui/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { Dialog, Card, Skeleton, Spinner, EmptyState, CopyButton, } from "./ui";
|
|
1
|
+
export { Dialog, Card, Skeleton, Spinner, EmptyState, CopyButton, Button, Badge, } from "./ui";
|
package/dist/ui/ui.css
CHANGED
|
@@ -13,6 +13,17 @@
|
|
|
13
13
|
.kit-empty-desc{font-size:14px;margin:0;max-width:34ch}
|
|
14
14
|
.kit-spinner{display:inline-block;box-sizing:border-box;border:2px solid var(--kit-border,#e5e7eb);border-top-color:currentColor;border-radius:50%;animation:kit-spin .7s linear infinite}
|
|
15
15
|
.kit-copy{display:inline-flex;align-items:center;gap:6px;border:0;background:transparent;color:inherit;font:inherit;cursor:pointer;padding:0}
|
|
16
|
+
.kit-button{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:9px 15px;border-radius:10px;font:inherit;font-weight:600;font-size:14px;cursor:pointer;transition:opacity .12s,transform .08s,background .12s;border:1px solid transparent}
|
|
17
|
+
.kit-button:disabled{opacity:.6;cursor:default}
|
|
18
|
+
.kit-button:active:not(:disabled){transform:translateY(1px)}
|
|
19
|
+
.kit-button-primary{background:var(--kit-accent,#111827);color:var(--kit-accent-fg,#fff)}
|
|
20
|
+
.kit-button-primary:hover:not(:disabled){opacity:.9}
|
|
21
|
+
.kit-button-secondary{background:var(--kit-row,#f3f4f6);color:var(--kit-fg,#111827)}
|
|
22
|
+
.kit-button-secondary:hover:not(:disabled){background:var(--kit-row-hover,#e9eaee)}
|
|
23
|
+
.kit-button-ghost{background:transparent;color:inherit;border-color:var(--kit-border,#e5e7eb)}
|
|
24
|
+
.kit-button-ghost:hover:not(:disabled){background:var(--kit-row,#f3f4f6)}
|
|
25
|
+
.kit-badge{display:inline-flex;align-items:center;gap:4px;padding:2px 9px;border-radius:999px;font:inherit;font-size:12px;font-weight:600;background:var(--kit-row,#f3f4f6);color:var(--kit-muted,#6b7280)}
|
|
26
|
+
@media (prefers-color-scheme:dark){.kit-button-secondary,.kit-badge{--kit-row:#27272c;--kit-row-hover:#313137;--kit-fg:#f5f5f7}}
|
|
16
27
|
@keyframes kit-fade{from{opacity:0}to{opacity:1}}
|
|
17
28
|
@keyframes kit-pop{from{opacity:0;transform:translateY(6px) scale(.98)}to{opacity:1;transform:none}}
|
|
18
29
|
@keyframes kit-spin{to{transform:rotate(360deg)}}
|
package/dist/ui/ui.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import "./ui.css";
|
|
2
|
-
import { type ReactNode, type CSSProperties, type HTMLAttributes } from "react";
|
|
2
|
+
import { type ReactNode, type CSSProperties, type HTMLAttributes, type ButtonHTMLAttributes } from "react";
|
|
3
|
+
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
4
|
+
variant?: "primary" | "secondary" | "ghost";
|
|
5
|
+
}
|
|
6
|
+
export declare function Button({ variant, className, ...rest }: ButtonProps): import("react").JSX.Element;
|
|
7
|
+
export declare function Badge({ className, ...rest }: HTMLAttributes<HTMLSpanElement>): import("react").JSX.Element;
|
|
3
8
|
export interface DialogProps {
|
|
4
9
|
open: boolean;
|
|
5
10
|
onClose: () => void;
|
package/dist/ui/ui.js
CHANGED
|
@@ -2,6 +2,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import "./ui.css";
|
|
3
3
|
import { useEffect, useRef, useState, } from "react";
|
|
4
4
|
import { cx } from "../util";
|
|
5
|
+
export function Button({ variant = "secondary", className, ...rest }) {
|
|
6
|
+
return _jsx("button", { className: cx("kit-button", "kit-button-" + variant, className), ...rest });
|
|
7
|
+
}
|
|
8
|
+
// ---- Badge ---------------------------------------------------------------------
|
|
9
|
+
export function Badge({ className, ...rest }) {
|
|
10
|
+
return _jsx("span", { className: cx("kit-badge", className), ...rest });
|
|
11
|
+
}
|
|
5
12
|
export function Dialog({ open, onClose, title, children, className }) {
|
|
6
13
|
const ref = useRef(null);
|
|
7
14
|
useEffect(() => {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import "./web3.css";
|
|
2
|
+
export interface AddressInputProps {
|
|
3
|
+
value: string;
|
|
4
|
+
onChange: (value: string) => void;
|
|
5
|
+
/** Fires with the resolved checksummed address (or null when invalid/unresolved). */
|
|
6
|
+
onResolved?: (address: `0x${string}` | null) => void;
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function AddressInput({ value, onChange, onResolved, placeholder, className }: AddressInputProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import "./web3.css";
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { useEnsAddress } from "wagmi";
|
|
5
|
+
import { isAddress } from "viem";
|
|
6
|
+
import { shortAddress } from "../format";
|
|
7
|
+
import { cx } from "../util";
|
|
8
|
+
// An address input that accepts a hex address OR an ENS name (`name.eth`), resolves
|
|
9
|
+
// it, and reports validity via [data-valid] + onResolved. Neutral; style .kit-addr-input.
|
|
10
|
+
export function AddressInput({ value, onChange, onResolved, placeholder = "0x… or name.eth", className }) {
|
|
11
|
+
const isEns = /\.eth$/i.test(value.trim());
|
|
12
|
+
const { data: resolved } = useEnsAddress({ name: isEns ? value.trim() : undefined, query: { enabled: isEns } });
|
|
13
|
+
const address = isAddress(value.trim())
|
|
14
|
+
? value.trim()
|
|
15
|
+
: (resolved ?? null);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
onResolved?.(address);
|
|
18
|
+
}, [address, onResolved]);
|
|
19
|
+
return (_jsxs("div", { className: cx("kit-addr-input", className), "data-valid": address ? "" : undefined, children: [_jsx("input", { value: value, onChange: (e) => onChange(e.target.value), placeholder: placeholder, spellCheck: false, autoComplete: "off" }), isEns && resolved ? _jsx("span", { className: "kit-addr-input-resolved", children: shortAddress(resolved) }) : null] }));
|
|
20
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import "./web3.css";
|
|
3
|
+
import { useChainId, useSwitchChain } from "wagmi";
|
|
4
|
+
import { cx } from "../util";
|
|
5
|
+
// A network picker — lists the chains in your wagmi config and switches on select.
|
|
6
|
+
// Neutral; style .kit-chain.
|
|
7
|
+
export function ChainSwitcher({ className }) {
|
|
8
|
+
const chainId = useChainId();
|
|
9
|
+
const { chains, switchChain, isPending } = useSwitchChain();
|
|
10
|
+
return (_jsx("select", { className: cx("kit-chain", className), value: chainId, disabled: isPending, onChange: (e) => switchChain({ chainId: Number(e.target.value) }), "aria-label": "Network", children: chains.map((c) => (_jsx("option", { value: c.id, children: c.name }, c.id))) }));
|
|
11
|
+
}
|
package/dist/web3/index.d.ts
CHANGED
|
@@ -2,3 +2,5 @@ export { Address, type AddressProps } from "./Address";
|
|
|
2
2
|
export { Balance, type BalanceProps } from "./Balance";
|
|
3
3
|
export { NetworkGuard, type NetworkGuardProps } from "./NetworkGuard";
|
|
4
4
|
export { TokenAmountInput, type TokenAmountInputProps } from "./TokenAmountInput";
|
|
5
|
+
export { ChainSwitcher, type ChainSwitcherProps } from "./ChainSwitcher";
|
|
6
|
+
export { AddressInput, type AddressInputProps } from "./AddressInput";
|
package/dist/web3/index.js
CHANGED
|
@@ -2,3 +2,5 @@ export { Address } from "./Address";
|
|
|
2
2
|
export { Balance } from "./Balance";
|
|
3
3
|
export { NetworkGuard } from "./NetworkGuard";
|
|
4
4
|
export { TokenAmountInput } from "./TokenAmountInput";
|
|
5
|
+
export { ChainSwitcher } from "./ChainSwitcher";
|
|
6
|
+
export { AddressInput } from "./AddressInput";
|
package/dist/web3/web3.css
CHANGED
|
@@ -12,4 +12,11 @@
|
|
|
12
12
|
.kit-amount-sym{color:var(--kit-muted,#6b7280);font-size:14px}
|
|
13
13
|
.kit-amount-max{border:0;background:transparent;color:var(--kit-muted,#6b7280);font:inherit;font-weight:600;font-size:12px;letter-spacing:.02em;text-transform:uppercase;cursor:pointer}
|
|
14
14
|
.kit-amount-max:hover{color:inherit}
|
|
15
|
-
|
|
15
|
+
.kit-chain{font:inherit;font-size:14px;padding:8px 10px;border:1px solid var(--kit-border,#e5e7eb);border-radius:10px;background:transparent;color:inherit;cursor:pointer}
|
|
16
|
+
.kit-addr-input{display:flex;align-items:center;gap:8px;border:1px solid var(--kit-border,#e5e7eb);border-radius:10px;padding:8px 10px;font:inherit}
|
|
17
|
+
.kit-addr-input:focus-within{border-color:var(--kit-muted,#6b7280)}
|
|
18
|
+
.kit-addr-input[data-valid]{border-color:var(--kit-success,#16a34a)}
|
|
19
|
+
.kit-addr-input input{flex:1;border:0;outline:0;background:transparent;color:inherit;font:inherit;font-size:15px;min-width:0;font-variant-numeric:tabular-nums}
|
|
20
|
+
.kit-addr-input-resolved{color:var(--kit-muted,#6b7280);font-size:13px;font-variant-numeric:tabular-nums}
|
|
21
|
+
.kit-async-error{color:var(--kit-error,#dc2626);font:inherit;font-size:14px}
|
|
22
|
+
@media (prefers-color-scheme:dark){.kit-net,.kit-amount,.kit-chain,.kit-addr-input{--kit-border:#2c2e36}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livo-build/kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Livo frontend kit — reusable React + wagmi v3 building blocks (wallet connect modal, providers, hooks) for web3 apps built on Livo.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|