Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/web-wallet/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%231a1a1a'/%3E%3Cg fill='%23f4b728'%3E%3Crect x='7' y='8' width='18' height='3'/%3E%3Cpolygon points='7,23 25,11 25,14 9,26'/%3E%3Crect x='7' y='23' width='18' height='3'/%3E%3C/g%3E%3C/svg%3E" />
<link href="src/styles/index.css" rel="stylesheet">
<title>WebZjs Web Wallet</title>
</head>
Expand Down
2 changes: 1 addition & 1 deletion packages/web-wallet/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
32 changes: 26 additions & 6 deletions packages/web-wallet/src/context/WebzjsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React, {
useReducer,
useEffect,
useCallback,
useRef,
} from 'react';
import { get } from 'idb-keyval';

Expand All @@ -26,6 +27,7 @@ export interface WebZjsState {
activeAccount?: number | null;
syncInProgress: boolean;
loading: boolean;
initialized: boolean;
}

type Action =
Expand All @@ -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,
Expand All @@ -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 {
Expand All @@ -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;
}
Expand All @@ -72,11 +78,13 @@ function reducer(state: WebZjsState, action: Action): WebZjsState {
interface WebZjsContextType {
state: WebZjsState;
dispatch: React.Dispatch<Action>;
initWallet: () => Promise<void>;
}

const WebZjsContext = createContext<WebZjsContextType>({
state: initialState,
dispatch: () => {},
initWallet: async () => {},
});

export function useWebZjsContext(): WebZjsContextType {
Expand All @@ -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 {
Expand Down Expand Up @@ -147,16 +156,27 @@ 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)) });
dispatch({ type: 'set-loading', payload: false });
}
}, []);

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) {
Expand All @@ -166,7 +186,7 @@ export const WebZjsProvider = ({ children }: { children: React.ReactNode }) => {


return (
<WebZjsContext.Provider value={{ state, dispatch }}>
<WebZjsContext.Provider value={{ state, dispatch, initWallet }}>
<Toaster />
{children}
</WebZjsContext.Provider>
Expand Down
36 changes: 31 additions & 5 deletions packages/web-wallet/src/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
}
Expand Down Expand Up @@ -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);
Expand All @@ -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 (
<div className="home-page flex items-start md:items-center justify-center px-4 overflow-y-hidden">
Expand Down