@livo-build/kit 0.2.0 → 0.2.2
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/dist/data/index.d.ts +1 -1
- package/dist/data/index.js +1 -1
- package/dist/data/useApi.d.ts +2 -0
- package/dist/data/useApi.js +16 -1
- package/dist/index.js +3 -2
- package/dist/polymarket/client.d.ts +6 -1
- package/dist/polymarket/client.js +29 -5
- package/dist/telegram/TelegramGate.d.ts +10 -0
- package/dist/telegram/TelegramGate.js +14 -0
- package/dist/telegram/TelegramLoginButton.d.ts +15 -0
- package/dist/telegram/TelegramLoginButton.js +21 -0
- package/dist/telegram/index.d.ts +3 -0
- package/dist/telegram/index.js +3 -0
- package/dist/telegram/telegram.css +4 -0
- package/dist/telegram/useTelegramLogin.d.ts +43 -0
- package/dist/telegram/useTelegramLogin.js +184 -0
- package/package.json +1 -1
package/dist/data/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { useApi, apiClient, type QueryResult, type UseApiOptions } from "./useApi";
|
|
1
|
+
export { useApi, apiClient, setApiAuthToken, type QueryResult, type UseApiOptions } from "./useApi";
|
|
2
2
|
export { useSubgraph, type UseSubgraphArgs } from "./useSubgraph";
|
|
3
3
|
export { Async, type AsyncProps, type AsyncQuery } from "./Async";
|
|
4
4
|
export { useCollection, processCollection, type Collection, type CollectionConfig, type SortState, type SortDir, } from "./useCollection";
|
package/dist/data/index.js
CHANGED
package/dist/data/useApi.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export interface QueryResult<T> {
|
|
|
5
5
|
isError: boolean;
|
|
6
6
|
refetch: () => void;
|
|
7
7
|
}
|
|
8
|
+
/** Set (or clear, with null) the bearer token sent on every same-origin api call. */
|
|
9
|
+
export declare function setApiAuthToken(token: string | null): void;
|
|
8
10
|
export interface UseApiOptions {
|
|
9
11
|
enabled?: boolean;
|
|
10
12
|
init?: RequestInit;
|
package/dist/data/useApi.js
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
// A bearer token applied to every same-origin api call (set by useTelegramLogin /
|
|
3
|
+
// any session hook). Sent as `Authorization: Bearer <token>` unless the call already
|
|
4
|
+
// supplies that header. `setApiAuthToken(null)` clears it (on logout).
|
|
5
|
+
let apiAuthToken = null;
|
|
6
|
+
/** Set (or clear, with null) the bearer token sent on every same-origin api call. */
|
|
7
|
+
export function setApiAuthToken(token) {
|
|
8
|
+
apiAuthToken = token;
|
|
9
|
+
}
|
|
2
10
|
// Call a Livo same-origin api/ backend ({slug}.livo.build/api/*). Paths are
|
|
3
11
|
// resolved under /api, so `useApi("/health")` hits /api/health — zero CORS, keys
|
|
4
12
|
// stay server-side. JSON in/out; a non-2xx throws with the body's `error` if present.
|
|
5
13
|
async function apiFetch(path, init) {
|
|
6
14
|
const p = path.startsWith("/api") ? path : "/api" + (path.startsWith("/") ? path : "/" + path);
|
|
7
|
-
|
|
15
|
+
let finalInit = init;
|
|
16
|
+
if (apiAuthToken) {
|
|
17
|
+
const headers = new Headers(init?.headers);
|
|
18
|
+
if (!headers.has("authorization"))
|
|
19
|
+
headers.set("authorization", `Bearer ${apiAuthToken}`);
|
|
20
|
+
finalInit = { ...init, headers };
|
|
21
|
+
}
|
|
22
|
+
const res = await fetch(p, finalInit);
|
|
8
23
|
const text = await res.text();
|
|
9
24
|
let body = null;
|
|
10
25
|
try {
|
package/dist/index.js
CHANGED
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
// hyperliquid/ — useHlPrice/Mids/Candles/OrderBook/Positions, PriceTicker (public market data, no key)
|
|
18
18
|
// account/ — Connected, Disconnected, RequireConnection, useIsConnected, Avatar, Identity
|
|
19
19
|
// ui/ — Dialog, Card, Skeleton, Spinner, EmptyState, CopyButton, Countdown
|
|
20
|
-
// telegram/ —
|
|
21
|
-
//
|
|
20
|
+
// telegram/ — useTelegramLogin/TelegramLoginButton (bot-driven web login, no wallet),
|
|
21
|
+
// useTelegramMiniApp, useTelegramLink, LinkTelegramButton, useTelegramTheme,
|
|
22
|
+
// useTelegramMainButton/BackButton/Haptics (login + Mini App + wallet ↔ Telegram linking)
|
|
22
23
|
// theme/ — KitThemeProvider (restyle the whole kit from one theme object)
|
|
23
24
|
// polymarket/ — useMarkets/useMarket/useMarketTrades/useOrderbook/usePriceHistory/usePlaceOrder,
|
|
24
25
|
// MarketsList/MarketCard/OddsBar/OrderTicket (prediction markets via Livo's indexer)
|
|
@@ -66,7 +66,12 @@ export interface PmHistoryPoint {
|
|
|
66
66
|
export declare function pmSnapshot(opts?: PmFeedOptions): Promise<PmSnapshot>;
|
|
67
67
|
/** Order-book depth for an outcome token (straight from the public CLOB). */
|
|
68
68
|
export declare function pmBook(tokenId: string, opts?: PmFeedOptions): Promise<PmBook>;
|
|
69
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Price history points for charting an outcome token. Prefers Livo's indexer (`/candles`,
|
|
71
|
+
* server-side CLOB egress on a trusted domain — works where the browser can't reach
|
|
72
|
+
* `clob.polymarket.com` directly), falling back to the public CLOB. Returns ascending
|
|
73
|
+
* `{ t, p }` points (`p` = the bucket close).
|
|
74
|
+
*/
|
|
70
75
|
export declare function pmPriceHistory(tokenId: string, params?: {
|
|
71
76
|
interval?: "1h" | "6h" | "1d" | "1w" | "max";
|
|
72
77
|
fidelity?: number;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// Frontend-safe Polymarket client. Reads Livo's Polymarket indexer
|
|
2
|
-
// (polymarket.livo.build) for the normalized markets + odds + trades feed
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
2
|
+
// (polymarket.livo.build) for the normalized markets + odds + trades feed AND for price
|
|
3
|
+
// history (`/candles`, fetched server-side from a Polymarket-permitted region) — so the
|
|
4
|
+
// browser never calls clob.polymarket.com directly, which fails on CORS / geoblocks /
|
|
5
|
+
// TLS-cert errors in some networks/regions. Order-book DEPTH (`pmBook`) still reads the
|
|
6
|
+
// public CLOB directly (the indexer exposes no per-token book), so it inherits those
|
|
7
|
+
// limits. For TRADING, route orders through your api/ Worker (usePlaceOrder), which signs
|
|
8
|
+
// server-side with @livo-build/runtime — never ship a trading key to the browser.
|
|
6
9
|
export const POLYMARKET_FEED = "https://polymarket.livo.build";
|
|
7
10
|
export const CLOB_HOST = "https://clob.polymarket.com";
|
|
8
11
|
const feedBase = (o) => (o?.feedUrl ?? POLYMARKET_FEED).replace(/\/$/, "");
|
|
@@ -21,8 +24,29 @@ export function pmSnapshot(opts) {
|
|
|
21
24
|
export function pmBook(tokenId, opts) {
|
|
22
25
|
return getJson(`${clobBase(opts)}/book?token_id=${encodeURIComponent(tokenId)}`);
|
|
23
26
|
}
|
|
24
|
-
/**
|
|
27
|
+
/** Span (seconds) of each `interval` preset — used to size the indexer `/candles` request. */
|
|
28
|
+
const INTERVAL_SPAN_S = { "1h": 3600, "6h": 21600, "1d": 86400, "1w": 604800, max: 0 };
|
|
29
|
+
/**
|
|
30
|
+
* Price history points for charting an outcome token. Prefers Livo's indexer (`/candles`,
|
|
31
|
+
* server-side CLOB egress on a trusted domain — works where the browser can't reach
|
|
32
|
+
* `clob.polymarket.com` directly), falling back to the public CLOB. Returns ascending
|
|
33
|
+
* `{ t, p }` points (`p` = the bucket close).
|
|
34
|
+
*/
|
|
25
35
|
export async function pmPriceHistory(tokenId, params = {}, opts) {
|
|
36
|
+
const bucket = Math.max(60, (params.fidelity ?? 60) * 60); // fidelity is in MINUTES
|
|
37
|
+
const span = INTERVAL_SPAN_S[params.interval ?? "1w"] ?? 604800;
|
|
38
|
+
const limit = span > 0 ? Math.max(1, Math.min(1000, Math.round(span / bucket))) : 1000; // "max" → cap at 1000
|
|
39
|
+
// 1) Livo indexer (trusted domain, server-side CLOB egress).
|
|
40
|
+
try {
|
|
41
|
+
const j = await getJson(`${feedBase(opts)}/candles?token_id=${encodeURIComponent(tokenId)}&interval=${bucket}&limit=${limit}`);
|
|
42
|
+
const candles = j.candles ?? [];
|
|
43
|
+
if (candles.length)
|
|
44
|
+
return candles.map((c) => ({ t: c.time, p: c.c }));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
/* indexer miss — fall back to the public CLOB below */
|
|
48
|
+
}
|
|
49
|
+
// 2) Public CLOB fallback (direct — may fail in blocked networks/regions).
|
|
26
50
|
const qs = new URLSearchParams({ market: tokenId, interval: params.interval ?? "1w" });
|
|
27
51
|
if (params.fidelity)
|
|
28
52
|
qs.set("fidelity", String(params.fidelity));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { type UseTelegramLoginOptions, type TelegramLoginUser } from "./useTelegramLogin";
|
|
3
|
+
export interface TelegramGateProps extends UseTelegramLoginOptions {
|
|
4
|
+
/** Shown once the visitor is signed in. A function receives the verified user. */
|
|
5
|
+
children: ReactNode | ((user: TelegramLoginUser | undefined) => ReactNode);
|
|
6
|
+
/** Optional override for the signed-out view (defaults to a <TelegramLoginButton />). */
|
|
7
|
+
fallback?: ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function TelegramGate({ children, fallback, className, ...opts }: TelegramGateProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useTelegramLogin } from "./useTelegramLogin";
|
|
3
|
+
import { TelegramLoginButton } from "./TelegramLoginButton";
|
|
4
|
+
// Drop-in "you must log in with Telegram to see this" wrapper — the whole flow in one
|
|
5
|
+
// component. Renders `children` when the visitor is authenticated, otherwise a
|
|
6
|
+
// "Continue with Telegram" button (or your `fallback`). For more control, build on
|
|
7
|
+
// useTelegramLogin directly. Secure by default (HttpOnly cookie session).
|
|
8
|
+
export function TelegramGate({ children, fallback, className, ...opts }) {
|
|
9
|
+
const { status, user } = useTelegramLogin(opts);
|
|
10
|
+
if (status === "authenticated") {
|
|
11
|
+
return _jsx(_Fragment, { children: typeof children === "function" ? children(user) : children });
|
|
12
|
+
}
|
|
13
|
+
return (_jsx("div", { className: className, "data-state": status, children: fallback ?? _jsx(TelegramLoginButton, { ...opts }) }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import "./telegram.css";
|
|
2
|
+
import { type UseTelegramLoginOptions } from "./useTelegramLogin";
|
|
3
|
+
export interface TelegramLoginButtonProps extends UseTelegramLoginOptions {
|
|
4
|
+
className?: string;
|
|
5
|
+
labels?: {
|
|
6
|
+
login?: string;
|
|
7
|
+
starting?: string;
|
|
8
|
+
open?: string;
|
|
9
|
+
authenticated?: (user?: {
|
|
10
|
+
username?: string;
|
|
11
|
+
id: number;
|
|
12
|
+
}) => string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export declare function TelegramLoginButton({ className, labels, ...opts }: TelegramLoginButtonProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import "./telegram.css";
|
|
3
|
+
import { useTelegramLogin } from "./useTelegramLogin";
|
|
4
|
+
import { cx } from "../util";
|
|
5
|
+
// One button for the whole "Login with Telegram" lifecycle: "Continue with Telegram"
|
|
6
|
+
// → (open web) an "Open Telegram" deep link plus the match code to confirm in the bot
|
|
7
|
+
// → an authenticated chip (tap to sign out). Inside a Mini App it logs in instantly.
|
|
8
|
+
// Neutral; style via .kit-tg / [data-state]. Build your own on useTelegramLogin for
|
|
9
|
+
// full control.
|
|
10
|
+
export function TelegramLoginButton({ className, labels, ...opts }) {
|
|
11
|
+
const { status, user, deepLink, matchCode, login, logout, error } = useTelegramLogin(opts);
|
|
12
|
+
if (status === "authenticated") {
|
|
13
|
+
const who = user?.username ? `@${user.username}` : user?.id ? `#${user.id}` : "";
|
|
14
|
+
return (_jsx("button", { className: cx("kit-tg", className), "data-state": "linked", onClick: () => logout(), children: labels?.authenticated ? labels.authenticated(user) : `Telegram ${who} ✓` }));
|
|
15
|
+
}
|
|
16
|
+
if (status === "awaiting" && deepLink) {
|
|
17
|
+
return (_jsxs("span", { className: cx("kit-tg-await", className), children: [_jsx("a", { className: "kit-tg", "data-state": "open", href: deepLink, target: "_blank", rel: "noreferrer", children: labels?.open ?? "Open Telegram to confirm" }), matchCode ? (_jsxs("small", { className: "kit-tg-code", children: ["Confirm code ", _jsx("code", { children: matchCode }), " in the bot"] })) : null] }));
|
|
18
|
+
}
|
|
19
|
+
const busy = status === "starting";
|
|
20
|
+
return (_jsx("button", { className: cx("kit-tg", className), "data-state": status === "error" ? "error" : status, disabled: busy, onClick: () => login(), title: error?.message, children: busy ? (labels?.starting ?? "Starting…") : (labels?.login ?? "Continue with Telegram") }));
|
|
21
|
+
}
|
package/dist/telegram/index.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export { useTelegramMiniApp, type TelegramMiniApp } from "./useTelegramMiniApp";
|
|
2
2
|
export { useTelegramLink, type TelegramLinkState, type LinkStatus, type LinkedTelegram, type UseTelegramLinkOptions, } from "./useTelegramLink";
|
|
3
3
|
export { LinkTelegramButton, type LinkTelegramButtonProps } from "./LinkTelegramButton";
|
|
4
|
+
export { useTelegramLogin, type TelegramLoginState, type TelegramLoginStatus, type TelegramLoginUser, type TelegramSessionTransport, type UseTelegramLoginOptions, } from "./useTelegramLogin";
|
|
5
|
+
export { TelegramLoginButton, type TelegramLoginButtonProps } from "./TelegramLoginButton";
|
|
6
|
+
export { TelegramGate, type TelegramGateProps } from "./TelegramGate";
|
|
4
7
|
export { useTelegramTheme } from "./useTelegramTheme";
|
|
5
8
|
export { useTelegramMainButton, useTelegramBackButton, useTelegramHaptics, type MainButtonOptions, type Haptics, } from "./miniapp";
|
package/dist/telegram/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export { useTelegramMiniApp } from "./useTelegramMiniApp";
|
|
2
2
|
export { useTelegramLink, } from "./useTelegramLink";
|
|
3
3
|
export { LinkTelegramButton } from "./LinkTelegramButton";
|
|
4
|
+
export { useTelegramLogin, } from "./useTelegramLogin";
|
|
5
|
+
export { TelegramLoginButton } from "./TelegramLoginButton";
|
|
6
|
+
export { TelegramGate } from "./TelegramGate";
|
|
4
7
|
export { useTelegramTheme } from "./useTelegramTheme";
|
|
5
8
|
export { useTelegramMainButton, useTelegramBackButton, useTelegramHaptics, } from "./miniapp";
|
|
@@ -3,4 +3,8 @@
|
|
|
3
3
|
.kit-tg:active{transform:translateY(1px)}
|
|
4
4
|
.kit-tg:disabled{opacity:.6;cursor:default}
|
|
5
5
|
.kit-tg[data-state="linked"]{background:var(--kit-row,#f3f4f6);color:var(--kit-fg,#111827)}
|
|
6
|
+
.kit-tg[data-state="error"]{background:var(--kit-danger,#dc2626);color:#fff}
|
|
6
7
|
@media (prefers-color-scheme:dark){.kit-tg[data-state="linked"]{--kit-row:#27272c;--kit-fg:#f5f5f7}}
|
|
8
|
+
.kit-tg-await{display:inline-flex;flex-direction:column;align-items:center;gap:6px}
|
|
9
|
+
.kit-tg-code{color:var(--kit-muted,#6b7280);font-size:12px}
|
|
10
|
+
.kit-tg-code code{background:var(--kit-row,#f3f4f6);padding:1px 6px;border-radius:6px;font-weight:700;letter-spacing:1px}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type TelegramLoginStatus = "anon" | "starting" | "awaiting" | "authenticated" | "error";
|
|
2
|
+
export type TelegramSessionTransport = "cookie" | "bearer";
|
|
3
|
+
export interface TelegramLoginUser {
|
|
4
|
+
id: number;
|
|
5
|
+
username?: string;
|
|
6
|
+
firstName?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface TelegramLoginState {
|
|
9
|
+
status: TelegramLoginStatus;
|
|
10
|
+
/** The verified Telegram identity, once authenticated. */
|
|
11
|
+
user?: TelegramLoginUser;
|
|
12
|
+
/** The signed session token — bearer transport only (cookie mode never exposes it to JS). */
|
|
13
|
+
token?: string;
|
|
14
|
+
/** Open-web flow: the t.me deep link to open the bot. */
|
|
15
|
+
deepLink?: string;
|
|
16
|
+
/** Short code shown on this page — the user confirms it matches the one the bot shows. */
|
|
17
|
+
matchCode?: string;
|
|
18
|
+
/** Begin a login. On a normal website this opens the bot; inside Telegram it's instant. */
|
|
19
|
+
login: () => Promise<void>;
|
|
20
|
+
/** Sign out — clears the session (cookie or stored token). */
|
|
21
|
+
logout: () => void;
|
|
22
|
+
error?: Error;
|
|
23
|
+
}
|
|
24
|
+
export interface UseTelegramLoginOptions {
|
|
25
|
+
/** Base path for the login API (default "/api/telegram/login"). */
|
|
26
|
+
endpoint?: string;
|
|
27
|
+
/**
|
|
28
|
+
* How the session is delivered. "cookie" (default) keeps the token out of JS — the
|
|
29
|
+
* server sets an HttpOnly cookie and the hook hydrates via the `/me` route; immune to
|
|
30
|
+
* XSS token theft (needs the api same-origin with the page, true on Livo). "bearer"
|
|
31
|
+
* stores the token in localStorage and attaches it to useApi calls (works cross-origin).
|
|
32
|
+
*/
|
|
33
|
+
transport?: TelegramSessionTransport;
|
|
34
|
+
/** "who am I" route for cookie-mode hydration (default `${endpoint}/me`). */
|
|
35
|
+
meEndpoint?: string;
|
|
36
|
+
/** localStorage key for the session (bearer transport only; default "livo.tg.session"). */
|
|
37
|
+
storageKey?: string;
|
|
38
|
+
/** Poll interval while waiting for the bot confirmation (default 2500ms). */
|
|
39
|
+
pollIntervalMs?: number;
|
|
40
|
+
/** Open the deep link automatically when login() starts (default true). */
|
|
41
|
+
autoOpen?: boolean;
|
|
42
|
+
}
|
|
43
|
+
export declare function useTelegramLogin(options?: UseTelegramLoginOptions): TelegramLoginState;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { useTelegramMiniApp } from "./useTelegramMiniApp";
|
|
3
|
+
import { setApiAuthToken } from "../data/useApi";
|
|
4
|
+
function loadSession(key) {
|
|
5
|
+
if (typeof window === "undefined")
|
|
6
|
+
return null;
|
|
7
|
+
try {
|
|
8
|
+
const raw = window.localStorage.getItem(key);
|
|
9
|
+
return raw ? JSON.parse(raw) : null;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// "Login with Telegram" for any web frontend — the user authenticates just by tapping
|
|
16
|
+
// your bot; no wallet, no password. On a normal website it opens a t.me deep link and
|
|
17
|
+
// polls until the user confirms in the bot (a match code defeats link-forwarding
|
|
18
|
+
// hijacks); inside a Telegram Mini App it verifies the signed initData instantly. The
|
|
19
|
+
// server (your api/) runs @livo-build/runtime's TelegramLogin and issues the session.
|
|
20
|
+
//
|
|
21
|
+
// Cookie transport (default) is the secure path: the session lives in an HttpOnly
|
|
22
|
+
// cookie the page JS never sees, so it can't be stolen via XSS — the hook learns who
|
|
23
|
+
// you are from the `/me` route. Switch to "bearer" only when the api is on a different
|
|
24
|
+
// origin; then the token is stored in localStorage and auto-attached to useApi calls.
|
|
25
|
+
export function useTelegramLogin(options = {}) {
|
|
26
|
+
const base = options.endpoint ?? "/api/telegram/login";
|
|
27
|
+
const transport = options.transport ?? "cookie";
|
|
28
|
+
const meEndpoint = options.meEndpoint ?? `${base}/me`;
|
|
29
|
+
const storageKey = options.storageKey ?? "livo.tg.session";
|
|
30
|
+
const pollMs = options.pollIntervalMs ?? 2500;
|
|
31
|
+
const autoOpen = options.autoOpen ?? true;
|
|
32
|
+
const mini = useTelegramMiniApp();
|
|
33
|
+
const [status, setStatus] = useState("anon");
|
|
34
|
+
const [user, setUser] = useState();
|
|
35
|
+
const [token, setToken] = useState();
|
|
36
|
+
const [deepLink, setDeepLink] = useState();
|
|
37
|
+
const [matchCode, setMatchCode] = useState();
|
|
38
|
+
const [error, setError] = useState();
|
|
39
|
+
const pollRef = useRef(null);
|
|
40
|
+
const stopPoll = () => {
|
|
41
|
+
if (pollRef.current) {
|
|
42
|
+
clearInterval(pollRef.current);
|
|
43
|
+
pollRef.current = null;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const authenticated = useCallback((who, tok) => {
|
|
47
|
+
stopPoll();
|
|
48
|
+
setUser(who);
|
|
49
|
+
setStatus("authenticated");
|
|
50
|
+
setDeepLink(undefined);
|
|
51
|
+
setMatchCode(undefined);
|
|
52
|
+
if (transport === "bearer" && tok) {
|
|
53
|
+
// Bearer: persist the token and attach it to every same-origin api call.
|
|
54
|
+
setApiAuthToken(tok);
|
|
55
|
+
setToken(tok);
|
|
56
|
+
if (typeof window !== "undefined") {
|
|
57
|
+
try {
|
|
58
|
+
window.localStorage.setItem(storageKey, JSON.stringify({ token: tok, user: who }));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
/* storage may be unavailable (private mode) — session still lives in memory */
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Cookie: nothing to store — the HttpOnly cookie is already set by the server.
|
|
66
|
+
}, [transport, storageKey]);
|
|
67
|
+
// Restore a session on mount: cookie mode asks the server (/me); bearer reads storage.
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
let cancelled = false;
|
|
70
|
+
if (transport === "cookie") {
|
|
71
|
+
(async () => {
|
|
72
|
+
try {
|
|
73
|
+
const r = await fetch(meEndpoint, { credentials: "same-origin" });
|
|
74
|
+
if (!r.ok)
|
|
75
|
+
return;
|
|
76
|
+
const j = (await r.json());
|
|
77
|
+
if (!cancelled && j.user) {
|
|
78
|
+
setUser(j.user);
|
|
79
|
+
setStatus("authenticated");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
/* not signed in / offline — stay anon */
|
|
84
|
+
}
|
|
85
|
+
})();
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
const saved = loadSession(storageKey);
|
|
89
|
+
if (saved?.token) {
|
|
90
|
+
setApiAuthToken(saved.token);
|
|
91
|
+
setToken(saved.token);
|
|
92
|
+
setUser(saved.user);
|
|
93
|
+
setStatus("authenticated");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return () => {
|
|
97
|
+
cancelled = true;
|
|
98
|
+
stopPoll();
|
|
99
|
+
};
|
|
100
|
+
}, [transport, meEndpoint, storageKey]);
|
|
101
|
+
const logout = useCallback(() => {
|
|
102
|
+
stopPoll();
|
|
103
|
+
setUser(undefined);
|
|
104
|
+
setStatus("anon");
|
|
105
|
+
setDeepLink(undefined);
|
|
106
|
+
setMatchCode(undefined);
|
|
107
|
+
if (transport === "bearer") {
|
|
108
|
+
setApiAuthToken(null);
|
|
109
|
+
setToken(undefined);
|
|
110
|
+
if (typeof window !== "undefined") {
|
|
111
|
+
try {
|
|
112
|
+
window.localStorage.removeItem(storageKey);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
/* ignore */
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// Cookie: ask the server to clear the HttpOnly cookie.
|
|
121
|
+
void fetch(`${base}/logout`, { method: "POST", credentials: "same-origin" }).catch(() => { });
|
|
122
|
+
}
|
|
123
|
+
}, [transport, base, storageKey]);
|
|
124
|
+
const login = useCallback(async () => {
|
|
125
|
+
setError(undefined);
|
|
126
|
+
setStatus("starting");
|
|
127
|
+
try {
|
|
128
|
+
// Inside Telegram (Mini App): we already have signed initData — verify it directly.
|
|
129
|
+
if (mini.available && mini.initData) {
|
|
130
|
+
const r = await fetch(`${base}/miniapp`, {
|
|
131
|
+
method: "POST",
|
|
132
|
+
credentials: "same-origin",
|
|
133
|
+
headers: { "content-type": "application/json" },
|
|
134
|
+
body: JSON.stringify({ initData: mini.initData }),
|
|
135
|
+
});
|
|
136
|
+
const j = (await r.json());
|
|
137
|
+
if (!r.ok || j.status !== "authenticated")
|
|
138
|
+
throw new Error(j.error ?? "Telegram login failed");
|
|
139
|
+
authenticated(j.user, j.token);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// Normal website: create a challenge, open the bot, poll until confirmed.
|
|
143
|
+
const startRes = await fetch(`${base}/start`, {
|
|
144
|
+
method: "POST",
|
|
145
|
+
credentials: "same-origin",
|
|
146
|
+
headers: { "content-type": "application/json" },
|
|
147
|
+
body: JSON.stringify({ origin: typeof window !== "undefined" ? window.location.origin : undefined }),
|
|
148
|
+
});
|
|
149
|
+
const start = (await startRes.json());
|
|
150
|
+
if (!startRes.ok || !start.code || !start.deepLink)
|
|
151
|
+
throw new Error(start.error ?? "Could not start login");
|
|
152
|
+
setDeepLink(start.deepLink);
|
|
153
|
+
setMatchCode(start.matchCode);
|
|
154
|
+
setStatus("awaiting");
|
|
155
|
+
if (autoOpen && typeof window !== "undefined")
|
|
156
|
+
window.open(start.deepLink, "_blank", "noopener");
|
|
157
|
+
const code = start.code;
|
|
158
|
+
stopPoll();
|
|
159
|
+
pollRef.current = setInterval(async () => {
|
|
160
|
+
try {
|
|
161
|
+
const r = await fetch(`${base}/poll?code=${encodeURIComponent(code)}`, { credentials: "same-origin" });
|
|
162
|
+
const j = (await r.json());
|
|
163
|
+
if (j.status === "authenticated") {
|
|
164
|
+
authenticated(j.user, j.token);
|
|
165
|
+
}
|
|
166
|
+
else if (j.status === "expired" || j.status === "unknown" || j.status === "used") {
|
|
167
|
+
stopPoll();
|
|
168
|
+
setStatus("error");
|
|
169
|
+
setError(new Error("Login expired — tap to try again."));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
/* transient network error — keep polling */
|
|
174
|
+
}
|
|
175
|
+
}, pollMs);
|
|
176
|
+
}
|
|
177
|
+
catch (e) {
|
|
178
|
+
stopPoll();
|
|
179
|
+
setError(e);
|
|
180
|
+
setStatus("error");
|
|
181
|
+
}
|
|
182
|
+
}, [base, mini.available, mini.initData, autoOpen, pollMs, authenticated]);
|
|
183
|
+
return { status, user, token, deepLink, matchCode, login, logout, error };
|
|
184
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livo-build/kit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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",
|