diff --git a/packages/web-wallet/index.html b/packages/web-wallet/index.html index 39d7b50..7201ce3 100644 --- a/packages/web-wallet/index.html +++ b/packages/web-wallet/index.html @@ -3,6 +3,7 @@ + WebZjs Web Wallet diff --git a/packages/web-wallet/src/App.tsx b/packages/web-wallet/src/App.tsx index 9e48408..69cd730 100644 --- a/packages/web-wallet/src/App.tsx +++ b/packages/web-wallet/src/App.tsx @@ -11,7 +11,7 @@ function App() { const { installedSnap } = useMetaMaskContext(); const { state } = useWebZjsContext(); - const interval = installedSnap && !state.syncInProgress ? RESCAN_INTERVAL : null; + const interval = installedSnap && state.initialized && !state.syncInProgress ? RESCAN_INTERVAL : null; useInterval(() => { triggerRescan(); diff --git a/packages/web-wallet/src/context/WebzjsContext.tsx b/packages/web-wallet/src/context/WebzjsContext.tsx index a2c31bd..1c11092 100644 --- a/packages/web-wallet/src/context/WebzjsContext.tsx +++ b/packages/web-wallet/src/context/WebzjsContext.tsx @@ -4,6 +4,7 @@ import React, { useReducer, useEffect, useCallback, + useRef, } from 'react'; import { get } from 'idb-keyval'; @@ -26,6 +27,7 @@ export interface WebZjsState { activeAccount?: number | null; syncInProgress: boolean; loading: boolean; + initialized: boolean; } type Action = @@ -35,7 +37,8 @@ type Action = | { type: 'set-chain-height'; payload: bigint } | { type: 'set-active-account'; payload: number } | { type: 'set-sync-in-progress'; payload: boolean } - | { type: 'set-loading'; payload: boolean }; + | { type: 'set-loading'; payload: boolean } + | { type: 'set-initialized'; payload: boolean }; const initialState: WebZjsState = { webWallet: null, @@ -45,7 +48,8 @@ const initialState: WebZjsState = { chainHeight: undefined, activeAccount: null, syncInProgress: false, - loading: true, + loading: false, + initialized: false, }; function reducer(state: WebZjsState, action: Action): WebZjsState { @@ -64,6 +68,8 @@ function reducer(state: WebZjsState, action: Action): WebZjsState { return { ...state, syncInProgress: action.payload }; case 'set-loading': return { ...state, loading: action.payload }; + case 'set-initialized': + return { ...state, initialized: action.payload }; default: return state; } @@ -72,11 +78,13 @@ function reducer(state: WebZjsState, action: Action): WebZjsState { interface WebZjsContextType { state: WebZjsState; dispatch: React.Dispatch; + initWallet: () => Promise; } const WebZjsContext = createContext({ state: initialState, dispatch: () => {}, + initWallet: async () => {}, }); export function useWebZjsContext(): WebZjsContextType { @@ -85,6 +93,7 @@ export function useWebZjsContext(): WebZjsContextType { export const WebZjsProvider = ({ children }: { children: React.ReactNode }) => { const [state, dispatch] = useReducer(reducer, initialState); + const initializingRef = useRef(false); const initAll = useCallback(async () => { try { @@ -147,6 +156,7 @@ export const WebZjsProvider = ({ children }: { children: React.ReactNode }) => { } dispatch({ type: 'set-loading', payload: false }); + dispatch({ type: 'set-initialized', payload: true }); } catch (err) { console.error('Initialization error:', err); dispatch({ type: 'set-error', payload: Error(String(err)) }); @@ -154,9 +164,19 @@ export const WebZjsProvider = ({ children }: { children: React.ReactNode }) => { } }, []); - useEffect(() => { - initAll().catch(console.error); - }, [initAll]); + // Lazy-load WASM: call this when user wants to use wallet features + const initWallet = useCallback(async () => { + if (state.initialized || initializingRef.current) { + return; // Already initialized or in progress + } + initializingRef.current = true; + dispatch({ type: 'set-loading', payload: true }); + try { + await initAll(); + } finally { + initializingRef.current = false; + } + }, [state.initialized, initAll]); useEffect(() => { if (state.error) { @@ -166,7 +186,7 @@ export const WebZjsProvider = ({ children }: { children: React.ReactNode }) => { return ( - + {children} diff --git a/packages/web-wallet/src/pages/Home.tsx b/packages/web-wallet/src/pages/Home.tsx index 0507460..a6b9357 100644 --- a/packages/web-wallet/src/pages/Home.tsx +++ b/packages/web-wallet/src/pages/Home.tsx @@ -8,7 +8,7 @@ import Loader from '../components/Loader/Loader'; const Home: React.FC = () => { const navigate = useNavigate(); - const { state, dispatch } = useWebZjsContext(); + const { state, dispatch, initWallet } = useWebZjsContext(); const { getAccountData, connectWebZjsSnap, recoverWallet } = useWebZjsActions(); const { installedSnap } = useMetaMask(); const { isPendingRequest } = useMetaMaskContext(); @@ -23,8 +23,19 @@ const Home: React.FC = () => { e.preventDefault(); connectingRef.current = true; try { + // Lazy-load WASM on first user interaction + await initWallet(); await connectWebZjsSnap(); navigate('/dashboard/account-summary'); + } catch (err: any) { + // Handle user rejection gracefully (code 4001) + if (err?.code === 4001) { + console.log('User rejected MetaMask connection'); + return; + } + // Other errors should be shown to user + console.error('Connection failed:', err); + dispatch({ type: 'set-error', payload: err instanceof Error ? err : new Error(String(err)) }); } finally { connectingRef.current = false; } @@ -52,9 +63,24 @@ const Home: React.FC = () => { return; } - // Case 2: No account but snap is installed - auto-recover (once only) - // Skip if connect is in progress to avoid duplicate viewingKey prompts - if (!recoveryAttemptedRef.current && !connectingRef.current) { + // Case 2: Wallet not initialized yet - initialize it first + if (!state.initialized && !connectingRef.current) { + try { + setRecovering(true); + await initWallet(); + // After init completes, effect will re-run with updated state + } catch (err) { + console.error('Wallet initialization failed:', err); + dispatch({ type: 'set-error', payload: err instanceof Error ? err : new Error(String(err)) }); + setShowResetInstructions(true); + } finally { + setRecovering(false); + } + return; + } + + // Case 3: Wallet initialized but no account - attempt recovery (once only) + if (state.initialized && !recoveryAttemptedRef.current && !connectingRef.current) { recoveryAttemptedRef.current = true; try { setRecovering(true); @@ -71,7 +97,7 @@ const Home: React.FC = () => { }; homeReload(); - }, [state.loading, state.activeAccount, installedSnap, navigate, dispatch, getAccountData, recoverWallet]); + }, [state.loading, state.activeAccount, state.initialized, installedSnap, navigate, dispatch, getAccountData, recoverWallet, initWallet]); return (