From 2df452d670fc9ab39f12a5235f55da177ff9b86c Mon Sep 17 00:00:00 2001 From: QSchlegel Date: Sat, 13 Jun 2026 08:46:14 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix(auth):=20recover=20from=20stuck=20"Load?= =?UTF-8?q?ing=E2=80=A6"=20when=20wallet=20connects=20but=20never=20gets?= =?UTF-8?q?=20a=20session?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a wallet connects but authorization doesn't complete (auto-authorize failed/cancelled — common when multiple Cardano extensions fight over window.cardano), layout sets hasCheckedSession=true and never reopens the auth modal. createUser stays 403 (no session), `user` stays null, and the Connect button spins "Loading…" forever with no recovery path. - Relabel that state: connected + user-query-resolved-empty now shows an actionable "Authorize" instead of an infinite "Loading…" spinner. - Add an "Authorize wallet" item to the connect dropdown (shown when connected but unauthorized) that bumps a new reauthNonce signal. - layout watches reauthNonce, clears the hasCheckedSession latch, and refetches the session so the existing session-check effect reopens the (now signData-fixed) WalletAuthModal. Additive and manual-trigger only — no change to connect/sign/session logic, no auto-retry loop. Co-Authored-By: Claude Fable 5 --- .../common/cardano-objects/connect-wallet.tsx | 30 ++++++++++++++++++- .../common/overall-layout/layout.tsx | 16 ++++++++++ src/lib/zustand/user.ts | 7 +++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/components/common/cardano-objects/connect-wallet.tsx b/src/components/common/cardano-objects/connect-wallet.tsx index 456d8752..be8c98dc 100644 --- a/src/components/common/cardano-objects/connect-wallet.tsx +++ b/src/components/common/cardano-objects/connect-wallet.tsx @@ -1,4 +1,4 @@ -import { Wallet, Loader2, CheckCircle2, AlertCircle } from "lucide-react"; +import { Wallet, Loader2, CheckCircle2, AlertCircle, ShieldCheck } from "lucide-react"; import { Button } from "@/components/ui/button"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { @@ -98,6 +98,7 @@ function ConnectWalletContent({ const { user, isLoading: isUserLoading } = useUser(); const userAddress = useUserStore((state) => state.userAddress); const setUserAddress = useUserStore((state) => state.setUserAddress); + const requestReauth = useUserStore((state) => state.requestReauth); const { toast } = useToast(); // Use WalletContext for regular wallet connection @@ -654,6 +655,17 @@ function ConnectWalletContent({ ); } + // Connected but the user query resolved with no user → no wallet session + // was established (authorization failed/cancelled). Don't spin forever: + // show an actionable "Authorize" label; the dropdown offers re-authorize. + if (isConnected && !user && !isUserLoading && !isConnecting) { + return ( + <> + + Authorize + + ); + } if (isConnected && isLoading) { return ( <> @@ -745,6 +757,22 @@ function ConnectWalletContent({ {isConnected && ( <> + {!user && !isUserLoading && ( + requestReauth()} + className={cn( + "px-3 py-2.5 rounded-md", + "text-zinc-900 dark:text-zinc-50", + "hover:bg-zinc-100 dark:hover:bg-zinc-800", + "focus:bg-zinc-100 dark:focus:bg-zinc-800", + "transition-colors duration-150", + "cursor-pointer" + )} + > + + Authorize wallet + + )} state.userAddress); const setUserAddress = useUserStore((state) => state.setUserAddress); + const reauthNonce = useUserStore((state) => state.reauthNonce); const ctx = api.useUtils(); // State for wallet authorization modal @@ -398,6 +399,21 @@ export default function RootLayout({ // Don't refetch here - let the natural query refetch handle it if needed }, []); + // Manual re-authorization: when the user is connected but never got a + // session (e.g. the auto-authorize failed/was cancelled), hasCheckedSession + // stays true and the modal never reopens — leaving the connect button stuck + // on "Loading…". Bumping reauthNonce (from the connect dropdown) clears that + // latch and refetches the session so the session-check effect reopens the + // auth modal. + useEffect(() => { + if (reauthNonce > 0) { + setHasCheckedSession(false); + setCheckingSession(false); + setShowAuthModal(false); + void refetchWalletSession(); + } + }, [reauthNonce, refetchWalletSession]); + const handleAuthModalAuthorized = useCallback(async () => { setShowAuthModal(false); setCheckingSession(false); diff --git a/src/lib/zustand/user.ts b/src/lib/zustand/user.ts index 076383cb..aadfe0ac 100644 --- a/src/lib/zustand/user.ts +++ b/src/lib/zustand/user.ts @@ -25,6 +25,11 @@ interface UserState { setPastWallet: (pastWallet: string | undefined) => void; pastUtxosEnabled: boolean; setPastUtxosEnabled: (enabled: boolean | ((prev: boolean) => boolean)) => void; + // Bumped when the user explicitly asks to re-run wallet authorization + // (e.g. after a failed/cancelled auto-authorize left them connected but + // unauthorized). The layout watches this to reopen the auth modal. + reauthNonce: number; + requestReauth: () => void; } export const useUserStore = create()( @@ -36,6 +41,8 @@ export const useUserStore = create()( setUser: (user) => set({ user }), pastWallet: undefined, setPastWallet: (wallet) => set({ pastWallet: wallet }), + reauthNonce: 0, + requestReauth: () => set((state) => ({ reauthNonce: state.reauthNonce + 1 })), pastUtxosEnabled: false, setPastUtxosEnabled: (enabled) => { const newValue = typeof enabled === "function" ? enabled(get().pastUtxosEnabled) : enabled; From c9af3416fb31fd6b1668134206eca55f4c4df39b Mon Sep 17 00:00:00 2001 From: QSchlegel Date: Sat, 13 Jun 2026 09:45:32 +0200 Subject: [PATCH 2/2] =?UTF-8?q?fix(auth):=20normalize=20all=20userAddress?= =?UTF-8?q?=20sources=20in=20layout=20to=20bech32=20(stuck=20"Loading?= =?UTF-8?q?=E2=80=A6")?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit THE root-cause fix for the stuck "Loading…": layout fed userAddress / the session check / createUser from hex-encoded addresses (react-2.0 useAddress() and raw CIP-30 getUsedAddresses/getUnusedAddresses), but user records and sessions are keyed by bech32 — so getUserByAddress(hex) returned null and the app hung. Confirmed live on preprod: getUserByAddress(addr1qyvgdy2…) finds the user; getUserByAddress(0118869144…) → null (same wallet). - Normalize `address` once at the useAddress() source so every consumer (store sync, walletAddressForSession/Check, createUser) uses bech32. - Normalize the getUsedAddresses()/getUnusedAddresses() setUserAddress sites. (The equivalent change was authored alongside #281 but merged after that PR was already squashed, so it never reached preprod — this carries it, plus the session/createUser hardening, with the required import.) Co-Authored-By: Claude Fable 5 --- src/components/common/overall-layout/layout.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/common/overall-layout/layout.tsx b/src/components/common/overall-layout/layout.tsx index ea38f0fa..8b86a74f 100644 --- a/src/components/common/overall-layout/layout.tsx +++ b/src/components/common/overall-layout/layout.tsx @@ -6,6 +6,7 @@ import { publicRoutes } from "@/data/public-routes"; import { api } from "@/utils/api"; import useUser from "@/hooks/useUser"; import { useUserStore } from "@/lib/zustand/user"; +import { normalizeAddressToBech32 } from "@/utils/addressCompatibility"; import useAppWallet from "@/hooks/useAppWallet"; import useUTXOS from "@/hooks/useUTXOS"; import useMeshWallet from "@/hooks/useMeshWallet"; @@ -102,7 +103,11 @@ export default function RootLayout({ // 1.9 IWallet bridge — used for getDRep(), which the react 2.0 wallet lacks. const { wallet: meshWallet } = useMeshWallet(); const { state: walletState, connectedWalletInstance } = useWalletContext(); - const address = useAddress(); + // react-2.0's useAddress can return hex-encoded address bytes; user records + // and sessions are keyed by bech32. Normalize once at the source so every + // consumer below (store sync, session check) uses the bech32 form. + const rawAddress = useAddress(); + const address = rawAddress ? normalizeAddressToBech32(rawAddress) : rawAddress; const { user, isLoading: isLoadingUser } = useUser(); const router = useRouter(); const { appWallet } = useAppWallet(); @@ -258,7 +263,7 @@ export default function RootLayout({ activeWallet.getUsedAddresses() .then((addresses) => { if (addresses && addresses.length > 0) { - setUserAddress(addresses[0]!); + setUserAddress(normalizeAddressToBech32(addresses[0]!)); fetchingAddressRef.current = false; } else { return activeWallet.getUnusedAddresses(); @@ -266,7 +271,7 @@ export default function RootLayout({ }) .then((addresses) => { if (addresses && addresses.length > 0 && !userAddress) { - setUserAddress(addresses[0]!); + setUserAddress(normalizeAddressToBech32(addresses[0]!)); } fetchingAddressRef.current = false; })