@livo-build/kit 0.1.0 → 0.1.6
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/auth/SignInWithEthereum.d.ts +8 -0
- package/dist/auth/SignInWithEthereum.js +18 -0
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.js +2 -0
- package/dist/auth/useSiwe.d.ts +18 -0
- package/dist/auth/useSiwe.js +72 -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/DataTable.d.ts +26 -0
- package/dist/data/DataTable.js +15 -0
- package/dist/data/data.css +22 -0
- package/dist/data/index.d.ts +5 -0
- package/dist/data/index.js +5 -0
- package/dist/data/useCollection.d.ts +42 -0
- package/dist/data/useCollection.js +67 -0
- package/dist/data/useCollection.test.d.ts +1 -0
- package/dist/data/useCollection.test.js +27 -0
- package/dist/data/useEventSource.d.ts +15 -0
- package/dist/data/useEventSource.js +37 -0
- package/dist/data/useWebSocket.d.ts +17 -0
- package/dist/data/useWebSocket.js +61 -0
- package/dist/dataviz/Stat.d.ts +18 -0
- package/dist/dataviz/Stat.js +14 -0
- package/dist/dataviz/dataviz.css +15 -0
- package/dist/dataviz/index.d.ts +2 -0
- package/dist/dataviz/index.js +2 -0
- package/dist/dataviz/sparkline.d.ts +15 -0
- package/dist/dataviz/sparkline.js +31 -0
- package/dist/dataviz/sparkline.test.d.ts +1 -0
- package/dist/dataviz/sparkline.test.js +14 -0
- package/dist/format.d.ts +3 -0
- package/dist/format.js +9 -0
- package/dist/hooks/index.d.ts +20 -0
- package/dist/hooks/index.js +97 -0
- package/dist/hyperliquid/PriceTicker.d.ts +11 -0
- package/dist/hyperliquid/PriceTicker.js +24 -0
- package/dist/hyperliquid/client.d.ts +9 -0
- package/dist/hyperliquid/client.js +17 -0
- package/dist/hyperliquid/hl.css +10 -0
- package/dist/hyperliquid/index.d.ts +3 -0
- package/dist/hyperliquid/index.js +3 -0
- package/dist/hyperliquid/useHyperliquid.d.ts +76 -0
- package/dist/hyperliquid/useHyperliquid.js +52 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +26 -5
- package/dist/nft/MintButton.d.ts +20 -0
- package/dist/nft/MintButton.js +9 -0
- package/dist/nft/NFTCard.d.ts +15 -0
- package/dist/nft/NFTCard.js +14 -0
- package/dist/nft/NFTMedia.d.ts +11 -0
- package/dist/nft/NFTMedia.js +16 -0
- package/dist/nft/index.d.ts +5 -0
- package/dist/nft/index.js +5 -0
- package/dist/nft/nft.css +16 -0
- package/dist/nft/resolveUri.d.ts +1 -0
- package/dist/nft/resolveUri.js +15 -0
- package/dist/nft/resolveUri.test.d.ts +1 -0
- package/dist/nft/resolveUri.test.js +13 -0
- package/dist/nft/useNFT.d.ts +34 -0
- package/dist/nft/useNFT.js +65 -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 +5 -0
- package/dist/telegram/index.js +5 -0
- package/dist/telegram/miniapp.d.ts +15 -0
- package/dist/telegram/miniapp.js +63 -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/telegram/useTelegramTheme.d.ts +2 -0
- package/dist/telegram/useTelegramTheme.js +20 -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/tx/TxProgress.d.ts +13 -0
- package/dist/tx/TxProgress.js +22 -0
- package/dist/tx/index.d.ts +1 -0
- package/dist/tx/index.js +1 -0
- package/dist/tx/tx.css +9 -0
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +1 -1
- package/dist/ui/ui.css +18 -0
- package/dist/ui/ui.d.ts +19 -1
- package/dist/ui/ui.js +31 -0
- package/dist/user/UserMenu.d.ts +10 -0
- package/dist/user/UserMenu.js +18 -0
- package/dist/user/index.d.ts +2 -0
- package/dist/user/index.js +2 -0
- package/dist/user/useUser.d.ts +32 -0
- package/dist/user/useUser.js +36 -0
- package/dist/user/user.css +15 -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/TokenAmount.d.ts +16 -0
- package/dist/web3/TokenAmount.js +17 -0
- package/dist/web3/index.d.ts +3 -0
- package/dist/web3/index.js +3 -0
- package/dist/web3/web3.css +12 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,18 +5,39 @@
|
|
|
5
5
|
// wallet/ — ConnectWallet, WalletModal, useWalletConnectors
|
|
6
6
|
// provider/ — LivoWeb3Provider (wagmi + react-query)
|
|
7
7
|
// contracts/ — useContractValue, createLivoContracts (bound to Livo's generated bindings)
|
|
8
|
-
// tx/ — useTx, TxButton, decodeRevert
|
|
8
|
+
// tx/ — useTx, TxButton, TxProgress, decodeRevert
|
|
9
|
+
// auth/ — useSiwe, SignInWithEthereum (Sign-In With Ethereum vs a same-origin /api/auth)
|
|
9
10
|
// toast/ — ToastProvider, useToast
|
|
10
|
-
// data/ — useApi (
|
|
11
|
-
// web3/ — Address, Balance, NetworkGuard,
|
|
12
|
-
//
|
|
13
|
-
//
|
|
11
|
+
// data/ — useApi, useSubgraph, useCollection + DataTable (search/sort/paginate), useEventSource/useWebSocket (realtime)
|
|
12
|
+
// web3/ — Address, Balance, NetworkGuard, TokenAmount(Input)
|
|
13
|
+
// token/ — useToken, useAllowance, useApprove, ApproveButton (ERC-20 + approvals)
|
|
14
|
+
// nft/ — useNFT, NFTCard, NFTMedia, MintButton (metadata + mint, ipfs-resolved)
|
|
15
|
+
// dataviz/ — Sparkline (inline SVG trend), Stat + StatGroup (dashboard metrics)
|
|
16
|
+
// user/ — useUser, UserMenu (wallet + SIWE session + Telegram link, unified)
|
|
17
|
+
// hyperliquid/ — useHlPrice/Mids/Candles/OrderBook/Positions, PriceTicker (public market data, no key)
|
|
18
|
+
// account/ — Connected, Disconnected, RequireConnection, useIsConnected, Avatar, Identity
|
|
19
|
+
// ui/ — Dialog, Card, Skeleton, Spinner, EmptyState, CopyButton, Countdown
|
|
20
|
+
// telegram/ — useTelegramMiniApp, useTelegramLink, LinkTelegramButton, useTelegramTheme,
|
|
21
|
+
// useTelegramMainButton/BackButton/Haptics (Mini App + wallet ↔ Telegram linking)
|
|
22
|
+
// theme/ — KitThemeProvider (restyle the whole kit from one theme object)
|
|
23
|
+
// hooks/ — useCopyToClipboard, useLocalStorage, useDebounce, useInterval, useCountdown
|
|
24
|
+
// format — shortAddress, formatToken, parseToken, formatUsd, formatPrice, timeAgo, …
|
|
14
25
|
export * from "./wallet";
|
|
15
26
|
export * from "./provider";
|
|
16
27
|
export * from "./contracts";
|
|
17
28
|
export * from "./tx";
|
|
29
|
+
export * from "./auth";
|
|
18
30
|
export * from "./toast";
|
|
19
31
|
export * from "./data";
|
|
20
32
|
export * from "./web3";
|
|
33
|
+
export * from "./token";
|
|
34
|
+
export * from "./nft";
|
|
35
|
+
export * from "./dataviz";
|
|
36
|
+
export * from "./user";
|
|
37
|
+
export * from "./hyperliquid";
|
|
38
|
+
export * from "./account";
|
|
21
39
|
export * from "./ui";
|
|
40
|
+
export * from "./telegram";
|
|
41
|
+
export * from "./theme";
|
|
42
|
+
export * from "./hooks";
|
|
22
43
|
export * from "./format";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { Abi } from "viem";
|
|
3
|
+
export interface MintButtonProps {
|
|
4
|
+
address: `0x${string}`;
|
|
5
|
+
abi: Abi;
|
|
6
|
+
/** Mint function name. Default "mint". */
|
|
7
|
+
functionName?: string;
|
|
8
|
+
/** Number to mint. Default 1. Passed as the first arg unless `args` is given. */
|
|
9
|
+
quantity?: number;
|
|
10
|
+
/** Price per unit in wei — total value sent = pricePerUnit * quantity. */
|
|
11
|
+
pricePerUnit?: bigint;
|
|
12
|
+
/** Explicit args (overrides the default `[quantity]`). */
|
|
13
|
+
args?: readonly unknown[];
|
|
14
|
+
className?: string;
|
|
15
|
+
children?: ReactNode;
|
|
16
|
+
toast?: boolean;
|
|
17
|
+
onSuccess?: (hash: `0x${string}`) => void;
|
|
18
|
+
onError?: (error: Error) => void;
|
|
19
|
+
}
|
|
20
|
+
export declare function MintButton({ address, abi, functionName, quantity, pricePerUnit, args, className, children, toast, onSuccess, onError, }: MintButtonProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import {} from "react";
|
|
3
|
+
import { TxButton } from "../tx/TxButton";
|
|
4
|
+
// A mint button: wraps TxButton with the common shape — value = pricePerUnit ×
|
|
5
|
+
// quantity, args default to [quantity]. Neutral styling (inherits .kit-btn).
|
|
6
|
+
export function MintButton({ address, abi, functionName = "mint", quantity = 1, pricePerUnit, args, className, children, toast, onSuccess, onError, }) {
|
|
7
|
+
const value = pricePerUnit !== undefined ? pricePerUnit * BigInt(quantity) : undefined;
|
|
8
|
+
return (_jsx(TxButton, { address: address, abi: abi, functionName: functionName, args: args ?? [BigInt(quantity)], value: value, className: className, toast: toast, onSuccess: onSuccess, onError: onError, children: children ?? `Mint${quantity > 1 ? ` ${quantity}` : ""}` }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import "./nft.css";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
import { type NftMetadata } from "./useNFT";
|
|
4
|
+
export interface NFTCardProps {
|
|
5
|
+
address?: `0x${string}`;
|
|
6
|
+
tokenId?: bigint | number;
|
|
7
|
+
standard?: "erc721" | "erc1155";
|
|
8
|
+
gateway?: string;
|
|
9
|
+
/** Show the trait/attribute chips. Default true. */
|
|
10
|
+
attributes?: boolean;
|
|
11
|
+
className?: string;
|
|
12
|
+
/** Custom render given the loaded metadata (overrides the default body). */
|
|
13
|
+
children?: (meta: NftMetadata | undefined) => ReactNode;
|
|
14
|
+
}
|
|
15
|
+
export declare function NFTCard({ address, tokenId, standard, gateway, attributes, className, children }: NFTCardProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import "./nft.css";
|
|
3
|
+
import {} from "react";
|
|
4
|
+
import { useNFT } from "./useNFT";
|
|
5
|
+
import { NFTMedia } from "./NFTMedia";
|
|
6
|
+
import { cx } from "../util";
|
|
7
|
+
// A neutral NFT card: media + name + (optional) attribute chips, loaded from the
|
|
8
|
+
// contract's token metadata. Style via the .kit-nft* classes — see STYLING.md.
|
|
9
|
+
export function NFTCard({ address, tokenId, standard, gateway, attributes = true, className, children }) {
|
|
10
|
+
const { metadata, image, isLoading, error } = useNFT({ address, tokenId, standard, gateway });
|
|
11
|
+
if (children)
|
|
12
|
+
return _jsx("div", { className: cx("kit-nft-card", className), children: children(metadata) });
|
|
13
|
+
return (_jsxs("div", { className: cx("kit-nft-card", className), "data-state": error ? "error" : isLoading ? "loading" : "ready", children: [_jsx(NFTMedia, { src: image, alt: metadata?.name, gateway: gateway }), _jsxs("div", { className: "kit-nft-body", children: [_jsx("div", { className: "kit-nft-name", children: metadata?.name ?? (isLoading ? "Loading…" : error ? "Unavailable" : "Untitled") }), attributes && metadata?.attributes?.length ? (_jsx("div", { className: "kit-nft-traits", children: metadata.attributes.map((a, i) => (_jsxs("span", { className: "kit-nft-trait", children: [a.trait_type ? _jsx("span", { className: "kit-nft-trait-k", children: a.trait_type }) : null, _jsx("span", { className: "kit-nft-trait-v", children: String(a.value) })] }, i))) })) : null] })] }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import "./nft.css";
|
|
2
|
+
export interface NFTMediaProps {
|
|
3
|
+
/** Media URL — ipfs://, ar://, http(s) or data: (resolved automatically). */
|
|
4
|
+
src?: string;
|
|
5
|
+
/** Treat as a <video> (autoplay/loop/muted). Default: infer from extension. */
|
|
6
|
+
video?: boolean;
|
|
7
|
+
alt?: string;
|
|
8
|
+
gateway?: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function NFTMedia({ src, video, alt, gateway, className }: NFTMediaProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import "./nft.css";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { resolveUri } from "./resolveUri";
|
|
5
|
+
import { cx } from "../util";
|
|
6
|
+
const VIDEO_RE = /\.(mp4|webm|ogg|mov)(\?|$)/i;
|
|
7
|
+
// Render NFT media from a (possibly ipfs://) URL: <img> or <video>, with a neutral
|
|
8
|
+
// skeleton until it loads and a fallback box on error. Style via .kit-nft-media*.
|
|
9
|
+
export function NFTMedia({ src, video, alt = "", gateway, className }) {
|
|
10
|
+
const [state, setState] = useState("loading");
|
|
11
|
+
if (!src)
|
|
12
|
+
return _jsx("div", { className: cx("kit-nft-media", "kit-nft-media-empty", className) });
|
|
13
|
+
const url = resolveUri(src, gateway);
|
|
14
|
+
const isVideo = video ?? VIDEO_RE.test(src);
|
|
15
|
+
return (_jsx("div", { className: cx("kit-nft-media", className), "data-state": state, children: isVideo ? (_jsx("video", { src: url, autoPlay: true, loop: true, muted: true, playsInline: true, onCanPlay: () => setState("ready"), onError: () => setState("error") })) : (_jsx("img", { src: url, alt: alt, loading: "lazy", onLoad: () => setState("ready"), onError: () => setState("error") })) }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { useNFT, type UseNFTParams, type NftResult, type NftMetadata, type NftAttribute } from "./useNFT";
|
|
2
|
+
export { NFTMedia, type NFTMediaProps } from "./NFTMedia";
|
|
3
|
+
export { NFTCard, type NFTCardProps } from "./NFTCard";
|
|
4
|
+
export { MintButton, type MintButtonProps } from "./MintButton";
|
|
5
|
+
export { resolveUri } from "./resolveUri";
|
package/dist/nft/nft.css
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/* NFT card + media. Neutral; restyle via these classes or --kit-* vars. */
|
|
2
|
+
.kit-nft-card{display:flex;flex-direction:column;gap:0;border:1px solid var(--kit-border,#e5e7eb);border-radius:var(--kit-radius,14px);overflow:hidden;background:var(--kit-bg,#fff);font:inherit}
|
|
3
|
+
.kit-nft-media{position:relative;aspect-ratio:1/1;background:var(--kit-skeleton,#f1f3f5);overflow:hidden}
|
|
4
|
+
.kit-nft-media img,.kit-nft-media video{width:100%;height:100%;object-fit:cover;display:block}
|
|
5
|
+
.kit-nft-media[data-state="loading"]{animation:kit-nft-pulse 1.2s ease-in-out infinite}
|
|
6
|
+
.kit-nft-media-empty{aspect-ratio:1/1;background:var(--kit-skeleton,#f1f3f5)}
|
|
7
|
+
@keyframes kit-nft-pulse{0%,100%{opacity:1}50%{opacity:.6}}
|
|
8
|
+
.kit-nft-body{display:flex;flex-direction:column;gap:8px;padding:12px}
|
|
9
|
+
.kit-nft-name{font-weight:600;font-size:15px;color:var(--kit-fg,#111827)}
|
|
10
|
+
.kit-nft-traits{display:flex;flex-wrap:wrap;gap:6px}
|
|
11
|
+
.kit-nft-trait{display:inline-flex;gap:4px;align-items:baseline;padding:3px 8px;border-radius:999px;background:var(--kit-skeleton,#f1f3f5);font-size:12px}
|
|
12
|
+
.kit-nft-trait-k{color:var(--kit-muted,#6b7280)}
|
|
13
|
+
.kit-nft-trait-v{font-weight:600;color:var(--kit-fg,#111827)}
|
|
14
|
+
@media (prefers-color-scheme:dark){
|
|
15
|
+
.kit-nft-card{--kit-border:#2c2e36;--kit-bg:#15171c;--kit-fg:#f5f5f7;--kit-skeleton:#23262e}
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function resolveUri(uri: string, gateway?: string): string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Resolve an ipfs:// / ar:// URI to an HTTP(S) URL. http(s)/data URIs pass through.
|
|
2
|
+
// (Mirrors @livo-build/runtime's resolveUri so the kit needs no runtime dependency.)
|
|
3
|
+
export function resolveUri(uri, gateway = "https://ipfs.io/ipfs/") {
|
|
4
|
+
if (!uri)
|
|
5
|
+
return uri;
|
|
6
|
+
if (uri.startsWith("ipfs://")) {
|
|
7
|
+
let rest = uri.slice("ipfs://".length);
|
|
8
|
+
if (rest.startsWith("ipfs/"))
|
|
9
|
+
rest = rest.slice("ipfs/".length);
|
|
10
|
+
return gateway + rest;
|
|
11
|
+
}
|
|
12
|
+
if (uri.startsWith("ar://"))
|
|
13
|
+
return "https://arweave.net/" + uri.slice("ar://".length);
|
|
14
|
+
return uri;
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { resolveUri } from "./resolveUri";
|
|
3
|
+
describe("resolveUri (kit)", () => {
|
|
4
|
+
it("resolves ipfs:// (and ipfs://ipfs/), ar://, and passes http/data through", () => {
|
|
5
|
+
expect(resolveUri("ipfs://bafyCID/1.png")).toBe("https://ipfs.io/ipfs/bafyCID/1.png");
|
|
6
|
+
expect(resolveUri("ipfs://ipfs/bafyCID")).toBe("https://ipfs.io/ipfs/bafyCID");
|
|
7
|
+
expect(resolveUri("ipfs://CID", "https://x.mypinata.cloud/ipfs/")).toBe("https://x.mypinata.cloud/ipfs/CID");
|
|
8
|
+
expect(resolveUri("ar://TXID")).toBe("https://arweave.net/TXID");
|
|
9
|
+
expect(resolveUri("https://cdn/x.png")).toBe("https://cdn/x.png");
|
|
10
|
+
expect(resolveUri("data:image/svg+xml,<svg/>")).toBe("data:image/svg+xml,<svg/>");
|
|
11
|
+
expect(resolveUri("")).toBe("");
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface NftAttribute {
|
|
2
|
+
trait_type?: string;
|
|
3
|
+
value?: unknown;
|
|
4
|
+
[k: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
export interface NftMetadata {
|
|
7
|
+
name?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
/** Image URL resolved through the gateway (ipfs:// → https). */
|
|
10
|
+
image?: string;
|
|
11
|
+
/** animation_url resolved through the gateway. */
|
|
12
|
+
animationUrl?: string;
|
|
13
|
+
attributes?: NftAttribute[];
|
|
14
|
+
raw: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
export interface UseNFTParams {
|
|
17
|
+
/** Collection contract address. */
|
|
18
|
+
address?: `0x${string}`;
|
|
19
|
+
tokenId?: bigint | number;
|
|
20
|
+
/** ERC-1155 reads uri(id); default ERC-721 tokenURI(id). */
|
|
21
|
+
standard?: "erc721" | "erc1155";
|
|
22
|
+
/** IPFS gateway base (trailing slash). Default "https://ipfs.io/ipfs/". */
|
|
23
|
+
gateway?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface NftResult {
|
|
26
|
+
metadata: NftMetadata | undefined;
|
|
27
|
+
/** Convenience: the resolved image URL from metadata. */
|
|
28
|
+
image: string | undefined;
|
|
29
|
+
/** The on-chain token URI (pre-fetch). */
|
|
30
|
+
uri: string | undefined;
|
|
31
|
+
isLoading: boolean;
|
|
32
|
+
error: Error | null;
|
|
33
|
+
}
|
|
34
|
+
export declare function useNFT(params: UseNFTParams): NftResult;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useReadContract } from "wagmi";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { resolveUri } from "./resolveUri";
|
|
4
|
+
const TOKEN_URI_ABI = [
|
|
5
|
+
{ type: "function", name: "tokenURI", stateMutability: "view", inputs: [{ name: "id", type: "uint256" }], outputs: [{ type: "string" }] },
|
|
6
|
+
{ type: "function", name: "uri", stateMutability: "view", inputs: [{ name: "id", type: "uint256" }], outputs: [{ type: "string" }] },
|
|
7
|
+
];
|
|
8
|
+
function idHex(id) {
|
|
9
|
+
return id.toString(16).padStart(64, "0");
|
|
10
|
+
}
|
|
11
|
+
async function loadMetadata(uri, gateway, id, standard) {
|
|
12
|
+
const sub = standard === "erc1155" ? uri.replace(/\{id\}/g, idHex(id)) : uri;
|
|
13
|
+
const resolved = resolveUri(sub, gateway);
|
|
14
|
+
let doc;
|
|
15
|
+
if (resolved.startsWith("data:")) {
|
|
16
|
+
const comma = resolved.indexOf(",");
|
|
17
|
+
const header = resolved.slice(5, comma);
|
|
18
|
+
const body = resolved.slice(comma + 1);
|
|
19
|
+
doc = JSON.parse(header.includes("base64") ? atob(body) : decodeURIComponent(body));
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
const res = await fetch(resolved);
|
|
23
|
+
if (!res.ok)
|
|
24
|
+
throw new Error(`metadata fetch ${res.status}`);
|
|
25
|
+
doc = (await res.json());
|
|
26
|
+
}
|
|
27
|
+
const imageRaw = (doc.image ?? doc.image_url ?? doc.imageUrl);
|
|
28
|
+
const animRaw = (doc.animation_url ?? doc.animationUrl);
|
|
29
|
+
return {
|
|
30
|
+
name: doc.name,
|
|
31
|
+
description: doc.description,
|
|
32
|
+
image: imageRaw ? resolveUri(imageRaw, gateway) : undefined,
|
|
33
|
+
animationUrl: animRaw ? resolveUri(animRaw, gateway) : undefined,
|
|
34
|
+
attributes: doc.attributes,
|
|
35
|
+
raw: doc,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// Read an NFT's on-chain metadata URI, then fetch + parse it (resolving ipfs:// →
|
|
39
|
+
// gateway for the doc and its image). Works for ERC-721 (tokenURI) and ERC-1155
|
|
40
|
+
// (uri(id) with {id} substitution). Caches via react-query.
|
|
41
|
+
export function useNFT(params) {
|
|
42
|
+
const { address, tokenId, standard = "erc721", gateway = "https://ipfs.io/ipfs/" } = params;
|
|
43
|
+
const id = tokenId === undefined ? undefined : BigInt(tokenId);
|
|
44
|
+
const fn = standard === "erc1155" ? "uri" : "tokenURI";
|
|
45
|
+
const uriRead = useReadContract({
|
|
46
|
+
address,
|
|
47
|
+
abi: TOKEN_URI_ABI,
|
|
48
|
+
functionName: fn,
|
|
49
|
+
args: id === undefined ? undefined : [id],
|
|
50
|
+
query: { enabled: Boolean(address && id !== undefined) },
|
|
51
|
+
});
|
|
52
|
+
const uri = uriRead.data;
|
|
53
|
+
const meta = useQuery({
|
|
54
|
+
queryKey: ["livo-nft", address, id?.toString(), uri, gateway],
|
|
55
|
+
queryFn: () => loadMetadata(uri, gateway, id, standard),
|
|
56
|
+
enabled: Boolean(uri),
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
metadata: meta.data,
|
|
60
|
+
image: meta.data?.image,
|
|
61
|
+
uri,
|
|
62
|
+
isLoading: uriRead.isLoading || meta.isLoading,
|
|
63
|
+
error: uriRead.error ?? meta.error ?? null,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -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,5 @@
|
|
|
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";
|
|
4
|
+
export { useTelegramTheme } from "./useTelegramTheme";
|
|
5
|
+
export { useTelegramMainButton, useTelegramBackButton, useTelegramHaptics, type MainButtonOptions, type Haptics, } from "./miniapp";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { useTelegramMiniApp } from "./useTelegramMiniApp";
|
|
2
|
+
export { useTelegramLink, } from "./useTelegramLink";
|
|
3
|
+
export { LinkTelegramButton } from "./LinkTelegramButton";
|
|
4
|
+
export { useTelegramTheme } from "./useTelegramTheme";
|
|
5
|
+
export { useTelegramMainButton, useTelegramBackButton, useTelegramHaptics, } from "./miniapp";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface MainButtonOptions {
|
|
2
|
+
text: string;
|
|
3
|
+
onClick: () => void;
|
|
4
|
+
show?: boolean;
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
progress?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function useTelegramMainButton(opts: MainButtonOptions): void;
|
|
9
|
+
export declare function useTelegramBackButton(onClick: () => void, show?: boolean): void;
|
|
10
|
+
export interface Haptics {
|
|
11
|
+
impact: (style?: "light" | "medium" | "heavy" | "rigid" | "soft") => void;
|
|
12
|
+
notification: (type?: "error" | "success" | "warning") => void;
|
|
13
|
+
selection: () => void;
|
|
14
|
+
}
|
|
15
|
+
export declare function useTelegramHaptics(): Haptics;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
function webApp() {
|
|
3
|
+
if (typeof window === "undefined")
|
|
4
|
+
return undefined;
|
|
5
|
+
return window.Telegram?.WebApp;
|
|
6
|
+
}
|
|
7
|
+
// Drive the Telegram Mini App MainButton from React. No-op outside Telegram.
|
|
8
|
+
export function useTelegramMainButton(opts) {
|
|
9
|
+
const cbRef = useRef(opts.onClick);
|
|
10
|
+
cbRef.current = opts.onClick;
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const mb = webApp()?.MainButton;
|
|
13
|
+
if (!mb)
|
|
14
|
+
return;
|
|
15
|
+
const handler = () => cbRef.current();
|
|
16
|
+
mb.setText(opts.text);
|
|
17
|
+
if (opts.enabled === false)
|
|
18
|
+
mb.disable();
|
|
19
|
+
else
|
|
20
|
+
mb.enable();
|
|
21
|
+
if (opts.progress)
|
|
22
|
+
mb.showProgress?.();
|
|
23
|
+
else
|
|
24
|
+
mb.hideProgress?.();
|
|
25
|
+
if (opts.show === false)
|
|
26
|
+
mb.hide();
|
|
27
|
+
else
|
|
28
|
+
mb.show();
|
|
29
|
+
mb.onClick(handler);
|
|
30
|
+
return () => {
|
|
31
|
+
mb.offClick(handler);
|
|
32
|
+
mb.hide();
|
|
33
|
+
};
|
|
34
|
+
}, [opts.text, opts.show, opts.enabled, opts.progress]);
|
|
35
|
+
}
|
|
36
|
+
// Drive the Telegram Mini App BackButton. No-op outside Telegram.
|
|
37
|
+
export function useTelegramBackButton(onClick, show = true) {
|
|
38
|
+
const cbRef = useRef(onClick);
|
|
39
|
+
cbRef.current = onClick;
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const bb = webApp()?.BackButton;
|
|
42
|
+
if (!bb)
|
|
43
|
+
return;
|
|
44
|
+
const handler = () => cbRef.current();
|
|
45
|
+
bb.onClick(handler);
|
|
46
|
+
if (show)
|
|
47
|
+
bb.show();
|
|
48
|
+
else
|
|
49
|
+
bb.hide();
|
|
50
|
+
return () => {
|
|
51
|
+
bb.offClick(handler);
|
|
52
|
+
bb.hide();
|
|
53
|
+
};
|
|
54
|
+
}, [show]);
|
|
55
|
+
}
|
|
56
|
+
// Telegram haptic feedback helpers. Safely no-op outside Telegram.
|
|
57
|
+
export function useTelegramHaptics() {
|
|
58
|
+
return {
|
|
59
|
+
impact: (style = "medium") => webApp()?.HapticFeedback?.impactOccurred(style),
|
|
60
|
+
notification: (type = "success") => webApp()?.HapticFeedback?.notificationOccurred(type),
|
|
61
|
+
selection: () => webApp()?.HapticFeedback?.selectionChanged(),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -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;
|