wallets-panel.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. "use client";
  2. /**
  3. * MetaMask + mavi shown the same way (logo, name, address, actions).
  4. */
  5. import { injected } from "@wagmi/connectors/injected";
  6. import Link from "next/link";
  7. import { useEffect, useState, type ReactNode } from "react";
  8. import {
  9. useAccount,
  10. useChainId,
  11. useConnect,
  12. useDisconnect,
  13. useSwitchChain,
  14. } from "wagmi";
  15. import { usePortalAuth } from "@/components/auth/portal-auth-provider";
  16. import { Button, buttonVariants } from "@/components/ui/button";
  17. import {
  18. MetaMaskLogo,
  19. MaviWalletLogo,
  20. } from "@/components/wallet/wallet-logos";
  21. import { createDemoWalletSession } from "@/lib/demo-session";
  22. import { hasVault, readVault, shortAddress } from "@/lib/mavi/storage";
  23. import { establishPortalSession } from "@/lib/portal-session";
  24. import { cn } from "@/lib/utils";
  25. export function WalletsPanel({
  26. className,
  27. /** Also open portal session when MetaMask connects */
  28. asLogin = false,
  29. onConnected,
  30. }: {
  31. className?: string;
  32. asLogin?: boolean;
  33. onConnected?: (address: string) => void;
  34. }) {
  35. const { address, isConnected, isConnecting } = useAccount();
  36. const { connect, error, isPending } = useConnect();
  37. const { disconnect } = useDisconnect();
  38. const chainId = useChainId();
  39. const { switchChain } = useSwitchChain();
  40. const { setDemoSession, clearDemo, demoSession } = usePortalAuth();
  41. const [maviAddr, setMaviAddr] = useState<string | null>(null);
  42. const [maviSaved, setMaviSaved] = useState(false);
  43. useEffect(() => {
  44. const vault = readVault();
  45. setMaviSaved(hasVault());
  46. setMaviAddr(vault?.address ?? null);
  47. }, []);
  48. function handleConnect() {
  49. connect(
  50. { connector: injected() },
  51. {
  52. onSuccess: (data) => {
  53. const addr = data.accounts[0];
  54. if (addr) {
  55. setDemoSession(createDemoWalletSession(addr));
  56. if (asLogin) {
  57. void establishPortalSession({
  58. userId: `wallet_${addr.toLowerCase()}`,
  59. method: "wallet",
  60. walletAddress: addr,
  61. }).then(() => {
  62. window.dispatchEvent(new Event("krypco-auth-change"));
  63. onConnected?.(addr);
  64. });
  65. } else {
  66. onConnected?.(addr);
  67. }
  68. }
  69. },
  70. },
  71. );
  72. }
  73. function handleDisconnect() {
  74. disconnect();
  75. if (demoSession?.kind === "wallet") {
  76. clearDemo();
  77. }
  78. }
  79. const mmConnected = isConnected && Boolean(address);
  80. return (
  81. <div className={cn("space-y-3", className)}>
  82. {/* MetaMask — same card shape as mavi */}
  83. <WalletRow
  84. logo={<MetaMaskLogo size={22} />}
  85. name="MetaMask"
  86. status={mmConnected ? "connected" : "not connected"}
  87. address={mmConnected ? address! : null}
  88. detail={mmConnected ? `chain id ${chainId}` : "Browser extension"}
  89. actions={
  90. mmConnected ? (
  91. <>
  92. <Button
  93. type="button"
  94. variant="outline"
  95. size="sm"
  96. onClick={handleDisconnect}
  97. >
  98. Disconnect
  99. </Button>
  100. {chainId !== 31337 && switchChain ? (
  101. <Button
  102. type="button"
  103. variant="ghost"
  104. size="sm"
  105. onClick={() => switchChain({ chainId: 31337 })}
  106. >
  107. Switch to local anvil
  108. </Button>
  109. ) : null}
  110. </>
  111. ) : (
  112. <Button
  113. type="button"
  114. size="sm"
  115. disabled={isConnecting || isPending}
  116. onClick={handleConnect}
  117. >
  118. {isConnecting || isPending ? "Connecting…" : "Connect"}
  119. </Button>
  120. )
  121. }
  122. />
  123. {/* mavi — same card shape */}
  124. <WalletRow
  125. logo={<MaviWalletLogo size={22} />}
  126. name="mavi wallet"
  127. status={maviSaved ? "saved on this browser" : "not created"}
  128. address={maviSaved && maviAddr ? maviAddr : null}
  129. detail={
  130. maviSaved
  131. ? "24-word recovery · encrypted here"
  132. : "Create + back up 24 words"
  133. }
  134. actions={
  135. maviSaved ? (
  136. <Link
  137. href="/portal/mavi"
  138. className={cn(
  139. buttonVariants({ variant: "outline", size: "sm" }),
  140. "inline-flex items-center gap-2",
  141. )}
  142. >
  143. <MaviWalletLogo size={16} />
  144. mavi wallet
  145. </Link>
  146. ) : (
  147. <>
  148. <Link
  149. href="/portal/mavi/create"
  150. className={cn(
  151. buttonVariants({ size: "sm" }),
  152. "inline-flex items-center gap-2",
  153. )}
  154. >
  155. <MaviWalletLogo size={16} />
  156. mavi wallet
  157. </Link>
  158. <Link
  159. href="/portal/mavi/import"
  160. className={cn(buttonVariants({ variant: "ghost", size: "sm" }))}
  161. >
  162. Import
  163. </Link>
  164. </>
  165. )
  166. }
  167. />
  168. {error ? (
  169. <p className="text-sm text-pending" role="alert">
  170. {error.message}
  171. </p>
  172. ) : null}
  173. </div>
  174. );
  175. }
  176. function WalletRow({
  177. logo,
  178. name,
  179. status,
  180. address,
  181. detail,
  182. actions,
  183. }: {
  184. logo: ReactNode;
  185. name: string;
  186. status: string;
  187. address: string | null;
  188. detail: string;
  189. actions: ReactNode;
  190. }) {
  191. return (
  192. <div className="rounded-xl border border-border bg-muted/20 px-3 py-3">
  193. <div className="flex items-start gap-3">
  194. <div className="mt-0.5 shrink-0">{logo}</div>
  195. <div className="min-w-0 flex-1">
  196. <div className="flex flex-wrap items-center gap-2">
  197. <p className="text-sm font-medium text-foreground">{name}</p>
  198. <span className="rounded-full border border-border px-2 py-0.5 text-[10px] uppercase tracking-wide text-muted-foreground">
  199. {status}
  200. </span>
  201. </div>
  202. {address ? (
  203. <p className="mt-1 break-all font-mono text-xs text-foreground">
  204. {address}
  205. </p>
  206. ) : (
  207. <p className="mt-1 font-mono text-xs text-muted-foreground">
  208. No address yet
  209. </p>
  210. )}
  211. <p className="mt-0.5 text-[11px] text-muted-foreground">{detail}</p>
  212. {address ? (
  213. <p className="mt-0.5 text-[11px] text-muted-foreground">
  214. Short: {shortAddress(address)}
  215. </p>
  216. ) : null}
  217. <div className="mt-2 flex flex-wrap gap-2">{actions}</div>
  218. </div>
  219. </div>
  220. </div>
  221. );
  222. }