WizardConnect/packages/react/src/hooks/useWizardConnect.ts

183 lines
5.1 KiB
TypeScript
Raw Normal View History

// Copyright (C) 2026 Whiterun LLC,
// This software is licensed under the GNU Lesser General Public License (LGPL), version 3.0 or later.
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/lgpl-3.0.html
import { useState, useRef, useEffect, useCallback } from "react";
import {
initiateDappRelay,
type DappRelayResult,
type RelayUpdatePayload,
} from "@wizardconnect/core";
import { DappConnectionManager, loadSession } from "@wizardconnect/dapp";
import type {
UseWizardConnectOptions,
UseWizardConnectResult,
WizardConnectState,
} from "../types.js";
const DEFAULT_SESSION_KEY = "wizardconnect-session";
/**
* React hook that encapsulates the WizardConnect relay lifecycle.
*
* Manages: relay initiation, DappConnectionManager, key exchange events,
* session persistence, and auto-reconnect on page refresh.
*
* The returned `manager` can be used to build an app-specific wallet adapter.
*/
export function useWizardConnect(
options: UseWizardConnectOptions = {},
): UseWizardConnectResult {
const {
dappName,
dappIcon,
relayUrls,
sessionKey = DEFAULT_SESSION_KEY,
persistSession = true,
storage,
} = options;
const [state, setState] = useState<WizardConnectState>("idle");
const [manager, setManager] = useState<DappConnectionManager | null>(null);
const [uri, setUri] = useState<string | null>(null);
const [qrUri, setQrUri] = useState<string | null>(null);
const [walletName, setWalletName] = useState<string | null>(null);
const [walletIcon, setWalletIcon] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const relayRef = useRef<DappRelayResult | null>(null);
const managerRef = useRef<DappConnectionManager | null>(null);
const autoReconnectAttempted = useRef(false);
const startRelay = useCallback(
(existingCredentials?: {
privateKey: string;
secret: string;
walletPublicKey: string;
}): boolean => {
if (state === "connecting" || state === "connected") return false;
setError(null);
setState("connecting");
const mgr = new DappConnectionManager(dappName, dappIcon, {
session: persistSession ? { key: sessionKey, storage } : false,
});
managerRef.current = mgr;
setManager(mgr);
mgr.on("walletready", () => {
setWalletName(mgr.walletName);
setWalletIcon(mgr.walletIcon);
setState("connected");
});
mgr.on("reconnecting", () => {
setState("reconnecting");
});
mgr.on("disconnect", () => {
setState("disconnected");
setWalletName(null);
setWalletIcon(null);
mgr.clearStoredSession();
relayRef.current?.cleanup();
relayRef.current = null;
});
try {
const relay = initiateDappRelay(
(payload: RelayUpdatePayload) => {
mgr.updateConnection(payload.client, payload.status);
},
{
existingCredentials,
explicitRelayUrls: relayUrls,
},
);
relayRef.current = relay;
// Only expose URI for new connections (QR pairing), not reconnects
if (!existingCredentials) {
setUri(relay.uri);
setQrUri(relay.qrUri);
}
mgr.attachRelay(relay);
return true;
} catch (e) {
const message =
e instanceof Error ? e.message : "Failed to start relay";
setError(message);
setState("idle");
return false;
}
},
[state, dappName, dappIcon, relayUrls, sessionKey, persistSession, storage],
);
const connect = useCallback((): boolean => {
return startRelay();
}, [startRelay]);
const disconnect = useCallback(async (): Promise<void> => {
try {
if (managerRef.current) {
await managerRef.current.sendDisconnect().catch(() => {});
}
} finally {
relayRef.current?.cleanup();
relayRef.current = null;
managerRef.current?.clearStoredSession();
managerRef.current = null;
setManager(null);
setUri(null);
setQrUri(null);
setWalletName(null);
setWalletIcon(null);
setState("idle");
}
}, []);
// Auto-reconnect on mount if a stored session exists
useEffect(() => {
if (autoReconnectAttempted.current) return;
if (!persistSession) return;
autoReconnectAttempted.current = true;
// Read session before creating the manager (need credentials for startRelay)
const stored = loadSession(sessionKey, storage);
if (!stored || !stored.walletPublicKey) return;
if (stored.walletName) {
setWalletName(stored.walletName);
}
startRelay({
privateKey: stored.privateKey,
secret: stored.secret,
walletPublicKey: stored.walletPublicKey,
});
}, [persistSession, sessionKey, storage, startRelay]);
// Cleanup on unmount
useEffect(() => {
return () => {
managerRef.current?.destroy();
relayRef.current?.cleanup();
};
}, []);
return {
state,
manager,
uri,
qrUri,
walletName,
walletIcon,
connect,
disconnect,
error,
};
}