159 lines
4.0 KiB
TypeScript
159 lines
4.0 KiB
TypeScript
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, type WalletKind } from "./injected";
|
|
import { clearWalletToken, readWalletToken, writeWalletToken } from "./token";
|
|
|
|
type WalletStatus = "loading" | "loggedOut" | "loggedIn";
|
|
|
|
const localWalletTokenPrefix = "local-wallet:";
|
|
|
|
export function localWalletToken(wallet: string): string {
|
|
return `${localWalletTokenPrefix}${wallet}`;
|
|
}
|
|
|
|
function walletFromLocalToken(token: string): string | null {
|
|
return token.startsWith(localWalletTokenPrefix)
|
|
? token.slice(localWalletTokenPrefix.length)
|
|
: null;
|
|
}
|
|
|
|
type WalletContextValue = {
|
|
address: string | null;
|
|
token: string | null;
|
|
status: WalletStatus;
|
|
loginModalOpen: boolean;
|
|
openLoginModal: () => void;
|
|
closeLoginModal: () => void;
|
|
signInInjected: (kind?: WalletKind) => 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;
|
|
}
|
|
|
|
const localWallet = walletFromLocalToken(token);
|
|
if (localWallet) {
|
|
setAddress(localWallet);
|
|
setStatus("loggedIn");
|
|
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 (kind?: WalletKind) => {
|
|
try {
|
|
const res = await signInWithInjectedWallet(kind);
|
|
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;
|
|
}
|