Files
Arkie-Library-Frontend/src/wallet/WalletProvider.tsx

137 lines
3.5 KiB
TypeScript
Raw Normal View History

2026-06-02 00:28:22 +08:00
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
type ReactNode,
} from "react";
import { useToast } from "../components/Toast";
import { useI18n } from "../i18n";
import { fetchWalletMe } from "./api";
import { signInWithInjectedWallet } from "./injected";
import { clearWalletToken, readWalletToken, writeWalletToken } from "./token";
type WalletStatus = "loading" | "loggedOut" | "loggedIn";
type WalletContextValue = {
address: string | null;
token: string | null;
status: WalletStatus;
loginModalOpen: boolean;
openLoginModal: () => void;
closeLoginModal: () => void;
signInInjected: () => Promise<void>;
completeLogin: (token: string, wallet: string) => void;
logout: () => void;
};
const WalletContext = createContext<WalletContextValue | null>(null);
export function shortenAddress(address: string): string {
if (address.length <= 12) return address;
return `${address.slice(0, 6)}...${address.slice(-4)}`;
}
export function WalletProvider({ children }: { children: ReactNode }) {
const { t } = useI18n();
const { showToast } = useToast();
const [token, setToken] = useState<string | null>(() => readWalletToken());
const [address, setAddress] = useState<string | null>(null);
const [status, setStatus] = useState<WalletStatus>(
token ? "loading" : "loggedOut",
);
const [loginModalOpen, setLoginModalOpen] = useState(false);
useEffect(() => {
let cancelled = false;
if (!token) {
setStatus("loggedOut");
setAddress(null);
return;
}
setStatus("loading");
fetchWalletMe(token)
.then((me) => {
if (cancelled) return;
setAddress(me.wallet);
setStatus("loggedIn");
})
.catch(() => {
if (cancelled) return;
clearWalletToken();
setToken(null);
setAddress(null);
setStatus("loggedOut");
});
return () => {
cancelled = true;
};
}, [token]);
const completeLogin = useCallback((nextToken: string, wallet: string) => {
writeWalletToken(nextToken);
setToken(nextToken);
setAddress(wallet);
setStatus("loggedIn");
setLoginModalOpen(false);
}, []);
const signInInjected = useCallback(async () => {
try {
const res = await signInWithInjectedWallet();
completeLogin(res.token, res.wallet);
showToast(t("walletLoginSuccess"));
} catch (error) {
const message =
error instanceof Error ? error.message : t("walletLoginFailed");
showToast(message || t("walletLoginFailed"), "error");
throw error;
}
}, [completeLogin, showToast, t]);
const logout = useCallback(() => {
clearWalletToken();
setToken(null);
setAddress(null);
setStatus("loggedOut");
showToast(t("walletDisconnected"));
}, [showToast, t]);
const value = useMemo<WalletContextValue>(
() => ({
address,
token,
status,
loginModalOpen,
openLoginModal: () => setLoginModalOpen(true),
closeLoginModal: () => setLoginModalOpen(false),
signInInjected,
completeLogin,
logout,
}),
[
address,
completeLogin,
loginModalOpen,
logout,
signInInjected,
status,
token,
],
);
return (
<WalletContext.Provider value={value}>{children}</WalletContext.Provider>
);
}
export function useWallet() {
const ctx = useContext(WalletContext);
if (!ctx) throw new Error("useWallet must be used within WalletProvider");
return ctx;
}