diff --git a/frontend/next.config.ts b/frontend/next.config.ts index d7858d3..a5c09e5 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -8,12 +8,6 @@ const nextConfig: NextConfig = { turbopack: { root: path.resolve(__dirname), }, - // @stacks/connect is imported statically (so the wallet path ships in the - // initial client bundle and can't lose a lazy chunk across deploys), but - // Turbopack's server bundle can't wire its module graph during prerender - // ("module factory is not available"). Keep it external on the server — - // Node resolves it natively there. - serverExternalPackages: ["@stacks/connect"], }; export default nextConfig; diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 8069c0b..9a74d28 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -3,7 +3,6 @@ import { GeistSans } from "geist/font/sans"; import { GeistMono } from "geist/font/mono"; import { QueryProvider } from "@/providers/query-provider"; import { StacksProvider } from "@/providers/stacks-provider"; -import { StaleDeployGuard } from "@/components/stale-deploy-guard"; import { Toaster } from "sonner"; import "./globals.css"; @@ -29,7 +28,6 @@ export default function RootLayout({ className={`${GeistSans.variable} ${GeistMono.variable} antialiased bg-surface-0 text-zinc-100`} > - {children} { - const onError = (event: ErrorEvent) => { - if (isStaleChunk(event.error ?? event.message)) { - event.preventDefault(); - reloadOnceForNewDeploy(); - } - }; - const onRejection = (event: PromiseRejectionEvent) => { - if (isStaleChunk(event.reason)) { - event.preventDefault(); - reloadOnceForNewDeploy(); - } - }; - window.addEventListener("error", onError); - window.addEventListener("unhandledrejection", onRejection); - return () => { - window.removeEventListener("error", onError); - window.removeEventListener("unhandledrejection", onRejection); - }; - }, []); - - return null; -} diff --git a/frontend/src/hooks/use-stacks-tx.ts b/frontend/src/hooks/use-stacks-tx.ts index 142622a..c66adc3 100644 --- a/frontend/src/hooks/use-stacks-tx.ts +++ b/frontend/src/hooks/use-stacks-tx.ts @@ -4,16 +4,8 @@ import { useCallback, useState } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { PostConditionMode } from "@stacks/transactions"; -// Static import on purpose: keeps the signing path out of lazy chunks that -// deploys invalidate under open tabs (see stacks-provider.tsx). -import { request } from "@stacks/connect"; import { waitForTxConfirmation, clarityErrorMessage } from "@/lib/stacks"; -import { - isUserCancel, - isNoResponse, - isStaleChunk, - walletErrorDetail, -} from "@/lib/wallet-errors"; +import { isUserCancel, isNoResponse, walletErrorDetail } from "@/lib/wallet-errors"; type TxStatus = "idle" | "pending" | "confirming" | "success" | "error"; @@ -57,6 +49,8 @@ export function useStacksTx() { // JSON-RPC bridge. The legacy openContractCall/StacksProvider path is // deprecated by Leather and silently hangs (no popup, dangling // promise) on current wallet versions. + const { request } = await import("@stacks/connect"); + const callParams = { contract: `${options.contractAddress}.${options.contractName}` as `${string}.${string}`, @@ -148,13 +142,6 @@ export function useStacksTx() { setStatus("idle"); return null; } - if (isStaleChunk(err)) { - // A deploy replaced the bundle under this open tab — reload onto - // the new build instead of surfacing module internals. - toast.info("StackStream was updated — refreshing…"); - setTimeout(() => window.location.reload(), 800); - return null; - } setError(walletErrorDetail(err)); setStatus("error"); return null; diff --git a/frontend/src/lib/wallet-errors.ts b/frontend/src/lib/wallet-errors.ts index 310f030..4038792 100644 --- a/frontend/src/lib/wallet-errors.ts +++ b/frontend/src/lib/wallet-errors.ts @@ -45,14 +45,3 @@ export function walletErrorDetail(err: unknown): string { const msg = e.message?.slice(0, 140) ?? "Unknown error"; return e.code !== undefined ? `${msg} (code ${e.code})` : msg; } - -/** - * A lazily-loaded chunk vanished because a new deploy replaced the hashed - * bundle files while this tab was still running the old build. The only - * correct recovery is a one-time page reload onto the new build. - */ -export function isStaleChunk(err: unknown): boolean { - return /module factory|ChunkLoadError|Loading chunk|Failed to fetch dynamically imported module|Importing a module script failed/i.test( - asErrorLike(err).message ?? "" - ); -} diff --git a/frontend/src/providers/stacks-provider.tsx b/frontend/src/providers/stacks-provider.tsx index c4dc0e4..3920bba 100644 --- a/frontend/src/providers/stacks-provider.tsx +++ b/frontend/src/providers/stacks-provider.tsx @@ -9,11 +9,6 @@ * `StacksProvider` object, which left the previous `showConnect`/ * `openContractCall` calls hanging with no popup. * - * The library is imported STATICALLY on purpose: it ships in the initial - * bundle (~10 KB gz), so the wallet path can never lose its lazy chunk when - * a deploy replaces the hashed bundle files under an open tab ("module - * factory is not available"). - * * Connect is deliberately stateless-first: we clear any cached approval and * force the wallet chooser on every connect, so switching accounts in the * wallet (e.g. sender -> recipient) always takes effect. Cached data made @@ -23,19 +18,12 @@ */ import { type ReactNode, useEffect, useCallback } from "react"; -import { - connect, - disconnect as walletDisconnect, - isConnected, - getLocalStorage, -} from "@stacks/connect"; import { toast } from "sonner"; import { useWalletStore } from "@/stores/wallet-store"; import { isUserCancel, isNoResponse, isNoWalletFound, - isStaleChunk, walletErrorDetail, } from "@/lib/wallet-errors"; @@ -47,34 +35,37 @@ export function StacksProvider({ children }: { children: ReactNode }) { // Rehydrate connection on mount from @stacks/connect localStorage useEffect(() => { - try { - // Purge pre-v8 leftovers. Returning visitors carry the legacy - // UserSession blob, and our persisted store may claim "connected" - // from that era — without a matching v8 session the UI would lie - // and wallet actions would misbehave. + (async () => { try { - localStorage.removeItem("blockstack-session"); - localStorage.removeItem("blockstack-gaia-hub-config"); - } catch { - /* storage unavailable — nothing to clean */ - } + // Purge pre-v8 leftovers. Returning visitors carry the legacy + // UserSession blob, and our persisted store may claim "connected" + // from that era — without a matching v8 session the UI would lie + // and wallet actions would misbehave. + try { + localStorage.removeItem("blockstack-session"); + localStorage.removeItem("blockstack-gaia-hub-config"); + } catch { + /* storage unavailable — nothing to clean */ + } - if (isConnected()) { - const stx = getLocalStorage()?.addresses.stx.find((a) => - STX_ADDRESS_RE.test(a.address) - ); - if (stx) { - setAddress(stx.address); - return; + const { isConnected, getLocalStorage } = await import("@stacks/connect"); + if (isConnected()) { + const stx = getLocalStorage()?.addresses.stx.find((a) => + STX_ADDRESS_RE.test(a.address) + ); + if (stx) { + setAddress(stx.address); + return; + } } + // No live v8 session: make the UI agree (clears any stale + // persisted "connected" state from before the migration). + disconnect(); + } catch { + // Stale or incompatible connect data — start fresh + disconnect(); } - // No live v8 session: make the UI agree (clears any stale - // persisted "connected" state from before the migration). - disconnect(); - } catch { - // Stale or incompatible connect data — start fresh - disconnect(); - } + })(); }, [setAddress, disconnect]); return <>{children}; @@ -87,6 +78,10 @@ export function useStacksAuth() { const handleConnect = useCallback(async () => { setConnecting(true); try { + const { connect, disconnect: walletDisconnect } = await import( + "@stacks/connect" + ); + // Drop any cached approval/addresses so the wallet is always asked // fresh — otherwise a reconnect can silently return the previously // approved account even after the user switched accounts in the wallet. @@ -147,13 +142,6 @@ export function useStacksAuth() { ); } catch (err) { console.error("[wallet connect]", err); - if (isStaleChunk(err)) { - // A deploy replaced the bundle under this open tab — reload onto - // the new build instead of surfacing module internals. - toast.info("StackStream was updated — refreshing…"); - setTimeout(() => window.location.reload(), 800); - return; - } toast.error(`Wallet connection failed: ${walletErrorDetail(err)}`, { duration: 8000, }); @@ -162,11 +150,9 @@ export function useStacksAuth() { }, [setAddress, setConnecting]); const handleDisconnect = useCallback(() => { - try { + import("@stacks/connect").then(({ disconnect: walletDisconnect }) => { walletDisconnect(); - } catch { - /* nothing cached */ - } + }); disconnect(); }, [disconnect]);