connect-button.tsx 6.5 KB

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