@me2em/react 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ import {
2
+ useState,
3
+ useCallback,
4
+ useMemo,
5
+ type ReactNode,
6
+ } from 'react';
7
+ import { Identity } from '@me2em/core';
8
+ import { Me2emContext, type Me2emContextValue } from './context.js';
9
+
10
+ export function Me2emProvider({ children }: { children: ReactNode }) {
11
+ const [identity, setIdentity] = useState<Identity | null>(null);
12
+
13
+ const activateIdentity = useCallback(async (seed: Uint8Array): Promise<boolean> => {
14
+ try {
15
+ const id = await Identity.fromSeed(seed);
16
+ setIdentity(id);
17
+ return true;
18
+ } catch {
19
+ return false;
20
+ }
21
+ }, []);
22
+
23
+ const clearIdentity = useCallback(() => {
24
+ setIdentity(null);
25
+ }, []);
26
+
27
+ const value = useMemo<Me2emContextValue>(
28
+ () => ({ identity, hasIdentity: identity !== null, activateIdentity, clearIdentity }),
29
+ [identity, activateIdentity, clearIdentity]
30
+ );
31
+
32
+ return <Me2emContext.Provider value={value}>{children}</Me2emContext.Provider>;
33
+ }
@@ -0,0 +1,37 @@
1
+ // packages/react/src/react/PassphraseInput.tsx
2
+ import { useState, useCallback } from 'react';
3
+
4
+ export interface PassphraseInputProps {
5
+ onPassphraseChange: (p: string) => void;
6
+ showWarnings?: boolean;
7
+ }
8
+
9
+ export function PassphraseInput({
10
+ onPassphraseChange,
11
+ showWarnings = true,
12
+ }: PassphraseInputProps) {
13
+ const handleChange = useCallback(
14
+ (e: React.ChangeEvent<HTMLInputElement>) => {
15
+ const value = e.target.value.normalize('NFKC');
16
+ onPassphraseChange(value);
17
+ },
18
+ [onPassphraseChange]
19
+ );
20
+
21
+ return (
22
+ <div>
23
+ <input
24
+ type="password"
25
+ data-testid="passphrase"
26
+ aria-label="BIP39 passphrase"
27
+ onChange={handleChange}
28
+ />
29
+ {showWarnings && (
30
+ <div data-testid="warning">
31
+ Passphrase is case-sensitive and NOT recoverable. A different
32
+ passphrase silently derives a different wallet.
33
+ </div>
34
+ )}
35
+ </div>
36
+ );
37
+ }
@@ -0,0 +1,92 @@
1
+ // packages/react/src/react/SeedPhraseDisplay.tsx
2
+ import { useReducer, useState } from 'react';
3
+ import {
4
+ displayFlowReducer,
5
+ initialDisplayState,
6
+ } from '../headless/display-flow.js';
7
+
8
+ export interface SeedPhraseDisplayProps {
9
+ words: string[];
10
+ onDismiss?: () => void;
11
+ onCopyAllowed?: () => void;
12
+ }
13
+
14
+ export function SeedPhraseDisplay({
15
+ words,
16
+ onDismiss,
17
+ onCopyAllowed,
18
+ }: SeedPhraseDisplayProps) {
19
+ const [state, dispatch] = useReducer(displayFlowReducer, initialDisplayState);
20
+ const [copyFailed, setCopyFailed] = useState(false);
21
+
22
+ const { status, copyCount } = state;
23
+
24
+ const handleReveal = () => {
25
+ dispatch({ type: 'REVEAL', at: Date.now() });
26
+ };
27
+
28
+ const handleCopy = async () => {
29
+ if (status === 'revealed' && copyCount === 0) {
30
+ try {
31
+ await (navigator as any).clipboard.writeText(words.join(' '));
32
+ dispatch({ type: 'COPY', at: Date.now() });
33
+ setCopyFailed(false);
34
+ onCopyAllowed?.();
35
+ } catch {
36
+ setCopyFailed(true);
37
+ }
38
+ }
39
+ };
40
+
41
+ const handleDismiss = () => {
42
+ dispatch({ type: 'DISMISS' });
43
+ onDismiss?.();
44
+ };
45
+
46
+ if (status === 'hidden') {
47
+ return (
48
+ <button data-testid="reveal" onClick={handleReveal}>
49
+ Reveal
50
+ </button>
51
+ );
52
+ }
53
+
54
+ if (status === 'dismissed') {
55
+ return (
56
+ <div data-testid="dismissed">
57
+ Phrase dismissed
58
+ </div>
59
+ );
60
+ }
61
+
62
+ return (
63
+ <div>
64
+ <ol data-testid="words">
65
+ {words.map((word, i) => (
66
+ <li
67
+ key={i}
68
+ data-testid={`word-${i}`}
69
+ data-position={i}
70
+ >
71
+ {word}
72
+ </li>
73
+ ))}
74
+ </ol>
75
+ {copyFailed && (
76
+ <div data-testid="copy-error">
77
+ Copy failed — try again
78
+ </div>
79
+ )}
80
+ <button
81
+ data-testid="copy-all"
82
+ onClick={handleCopy}
83
+ disabled={copyCount === 1}
84
+ >
85
+ {copyCount === 1 ? 'Copied' : 'Copy all'}
86
+ </button>
87
+ <button data-testid="dismiss" onClick={handleDismiss}>
88
+ Hide
89
+ </button>
90
+ </div>
91
+ );
92
+ }
@@ -0,0 +1,110 @@
1
+ // packages/react/src/react/SeedPhraseImport.tsx
2
+ import { useReducer, useCallback, useEffect, useRef } from 'react';
3
+ import {
4
+ importFlowReducer,
5
+ initImportState,
6
+ type ImportState,
7
+ } from '../headless/import-flow.js';
8
+ import { wordlist } from '@me2em/core';
9
+
10
+ export interface SeedPhraseImportProps {
11
+ expectedCount?: 12 | 24;
12
+ wordlist?: readonly string[];
13
+ onImported: (words: string[]) => void;
14
+ }
15
+
16
+ export function SeedPhraseImport({
17
+ expectedCount: propCount,
18
+ wordlist: propWordlist,
19
+ onImported,
20
+ }: SeedPhraseImportProps) {
21
+ const allWords = propWordlist ?? wordlist;
22
+
23
+ const [state, dispatch] = useReducer(
24
+ importFlowReducer,
25
+ { expectedCount: propCount ?? 12 } as const,
26
+ (params) => initImportState(params.expectedCount)
27
+ );
28
+
29
+ const count = state.expectedCount;
30
+
31
+ const prevStatusRef = useRef<ImportState['status']>(state.status);
32
+
33
+ const lastImportedRef = useRef<string | null>(null);
34
+
35
+ // Auto-check on every word change
36
+ useEffect(() => {
37
+ dispatch({ type: 'CHECK' });
38
+ }, [state.words]);
39
+
40
+ // Call onImported once when transitioning to valid
41
+ useEffect(() => {
42
+ const prevStatus = prevStatusRef.current;
43
+ prevStatusRef.current = state.status;
44
+
45
+ if (state.status === 'valid' && prevStatus !== 'valid') {
46
+ const key = (state.words as string[]).join('\0');
47
+ if (lastImportedRef.current !== key) {
48
+ lastImportedRef.current = key;
49
+ onImported(state.words as string[]);
50
+ }
51
+ }
52
+ }, [state.status, state.words, onImported]);
53
+
54
+ const handleCountChange = (n: 12 | 24) => {
55
+ dispatch({ type: 'INIT', expectedCount: n });
56
+ };
57
+
58
+ const handleWordChange = (position: number, value: string) => {
59
+ dispatch({ type: 'SET_WORD', position, word: value });
60
+ };
61
+
62
+ const isWordInvalid = (position: number): boolean => {
63
+ if (state.status !== 'invalid' && state.status !== 'valid') return false;
64
+ const word = state.words[position];
65
+ if (word === null) return false;
66
+ return !allWords.includes(word);
67
+ };
68
+
69
+ const isImportEnabled = state.status === 'valid';
70
+
71
+ return (
72
+ <div>
73
+ <div>
74
+ <button
75
+ data-testid="count-12"
76
+ onClick={() => handleCountChange(12)}
77
+ disabled={count === 12}
78
+ >
79
+ 12 words
80
+ </button>
81
+ <button
82
+ data-testid="count-24"
83
+ onClick={() => handleCountChange(24)}
84
+ disabled={count === 24}
85
+ >
86
+ 24 words
87
+ </button>
88
+ </div>
89
+ <div data-testid="inputs">
90
+ {Array.from({ length: count }, (_, i) => (
91
+ <input
92
+ key={i}
93
+ data-testid={`word-${i}`}
94
+ aria-label={`Word ${i + 1}`}
95
+ value={state.words[i] ?? ''}
96
+ onChange={(e) => handleWordChange(i, e.target.value)}
97
+ data-invalid={isWordInvalid(i) ? 'true' : undefined}
98
+ />
99
+ ))}
100
+ </div>
101
+ <button
102
+ data-testid="import"
103
+ disabled={!isImportEnabled}
104
+ onClick={() => dispatch({ type: 'CHECK' })}
105
+ >
106
+ Import
107
+ </button>
108
+ </div>
109
+ );
110
+ }
@@ -0,0 +1,128 @@
1
+ // packages/react/src/react/SeedPhraseVerify.tsx
2
+ import { useReducer, useCallback, useState, useEffect, useRef } from 'react';
3
+ import {
4
+ verifyFlowReducer,
5
+ initVerifyState,
6
+ type VerifyState,
7
+ } from '../headless/verify-flow.js';
8
+ import { wordlist } from '@me2em/core';
9
+
10
+ export interface SeedPhraseVerifyProps {
11
+ realWords: string[];
12
+ gridSize?: number;
13
+ wordlist?: readonly string[];
14
+ onVerified: () => void;
15
+ onFailed?: (attempts: number) => void;
16
+ }
17
+
18
+ const DEFAULT_GRID_SIZE_OFFSET = 8;
19
+
20
+ export function SeedPhraseVerify({
21
+ realWords,
22
+ gridSize,
23
+ wordlist: propWordlist,
24
+ onVerified,
25
+ onFailed,
26
+ }: SeedPhraseVerifyProps) {
27
+ const grid = gridSize ?? realWords.length + DEFAULT_GRID_SIZE_OFFSET;
28
+ const allWords = propWordlist ?? wordlist;
29
+ const baseSeed = 42;
30
+
31
+ const [state, dispatch] = useReducer(
32
+ verifyFlowReducer,
33
+ {
34
+ realWords,
35
+ gridSize: grid,
36
+ randomSeed: baseSeed,
37
+ allWords,
38
+ } as const,
39
+ (params) => initVerifyState(params.realWords, params.gridSize, params.randomSeed, params.allWords)
40
+ );
41
+
42
+ const [retryCount, setRetryCount] = useState(0);
43
+
44
+ const { status, grid: gridData, selected } = state;
45
+
46
+ // Guard against re-entrant callbacks after status change
47
+ const notifiedRef = useRef(false);
48
+
49
+ const handleSelect = useCallback(
50
+ (index: number) => {
51
+ if (status === 'correct' || status === 'incorrect') return;
52
+ if (selected.length >= gridData.correctIndices.length) return;
53
+ dispatch({ type: 'SELECT', index });
54
+ },
55
+ [status, selected.length, gridData.correctIndices.length]
56
+ );
57
+
58
+ // Single effect: auto-check when enough words selected, then notify result
59
+ useEffect(() => {
60
+ if (notifiedRef.current) return;
61
+
62
+ // Auto-check when enough words selected
63
+ if (
64
+ selected.length === gridData.correctIndices.length &&
65
+ selected.length > 0 &&
66
+ status === 'collecting'
67
+ ) {
68
+ dispatch({ type: 'CHECK' });
69
+ // After dispatch, status will change synchronously in the reducer
70
+ // but the new state won't be reflected until next render.
71
+ // We need to check the pending result in the next effect cycle.
72
+ return;
73
+ }
74
+
75
+ // Handle result after status update
76
+ if (status === 'correct') {
77
+ notifiedRef.current = true;
78
+ onVerified();
79
+ } else if (status === 'incorrect') {
80
+ notifiedRef.current = true;
81
+ onFailed?.(state.attempts);
82
+ }
83
+ }, [selected.length, gridData.correctIndices.length, status, state.attempts, onVerified, onFailed]);
84
+
85
+ const handleRetry = () => {
86
+ notifiedRef.current = false;
87
+ setRetryCount((c) => c + 1);
88
+ dispatch({
89
+ type: 'RESET',
90
+ realWords,
91
+ gridSize: grid,
92
+ randomSeed: baseSeed + retryCount + 1,
93
+ allWords,
94
+ });
95
+ };
96
+
97
+ return (
98
+ <div>
99
+ <div data-testid="progress">
100
+ Select word {selected.length + 1} of {realWords.length}
101
+ </div>
102
+ <ol data-testid="grid">
103
+ {gridData.words.map((word, i) => {
104
+ const isSelected = selected.includes(i);
105
+ const isDisabled =
106
+ isSelected || status === 'correct' || status === 'incorrect';
107
+ return (
108
+ <li key={i}>
109
+ <button
110
+ data-testid={`cell-${i}`}
111
+ data-word={word}
112
+ disabled={isDisabled}
113
+ onClick={() => handleSelect(i)}
114
+ >
115
+ {word}
116
+ </button>
117
+ </li>
118
+ );
119
+ })}
120
+ </ol>
121
+ {status === 'incorrect' && (
122
+ <button data-testid="retry" onClick={handleRetry}>
123
+ Try again
124
+ </button>
125
+ )}
126
+ </div>
127
+ );
128
+ }
@@ -0,0 +1,19 @@
1
+ import { createContext, useContext } from 'react';
2
+ import type { Identity } from '@me2em/core';
3
+
4
+ export interface Me2emContextValue {
5
+ identity: Identity | null;
6
+ hasIdentity: boolean;
7
+ activateIdentity: (seed: Uint8Array) => Promise<boolean>;
8
+ clearIdentity: () => void;
9
+ }
10
+
11
+ export const Me2emContext = createContext<Me2emContextValue | null>(null);
12
+
13
+ export function useMe2emContext(): Me2emContextValue {
14
+ const ctx = useContext(Me2emContext);
15
+ if (!ctx) {
16
+ throw new Error('Me2em hooks must be used inside <Me2emProvider>');
17
+ }
18
+ return ctx;
19
+ }
@@ -0,0 +1,23 @@
1
+ export { Me2emProvider } from './Me2emProvider.js';
2
+ export { useMe2emContext } from './context.js';
3
+ export { useCreateIdentity } from './useCreateIdentity.js';
4
+ export { useImportSeed } from './useImportSeed.js';
5
+ export { useHandle } from './useHandle.js';
6
+ export { useSubHandle } from './useSubHandle.js';
7
+ export { SeedPhraseDisplay } from './SeedPhraseDisplay.js';
8
+ export type { SeedPhraseDisplayProps } from './SeedPhraseDisplay.js';
9
+ export { SeedPhraseVerify } from './SeedPhraseVerify.js';
10
+ export type { SeedPhraseVerifyProps } from './SeedPhraseVerify.js';
11
+ export { SeedPhraseImport } from './SeedPhraseImport.js';
12
+ export type { SeedPhraseImportProps } from './SeedPhraseImport.js';
13
+ export { PassphraseInput } from './PassphraseInput.js';
14
+ export type { PassphraseInputProps } from './PassphraseInput.js';
15
+ export { useIdentityContext } from './useIdentityContext.js';
16
+ export type {
17
+ UseIdentityContextResult,
18
+ } from './useIdentityContext.js';
19
+ export { useSession } from './useSession.js';
20
+ export type {
21
+ UseSessionOptions,
22
+ UseSessionResult,
23
+ } from './useSession.js';
@@ -0,0 +1,77 @@
1
+ // packages/react/src/react/useCreateIdentity.ts
2
+ import {
3
+ useReducer,
4
+ useCallback,
5
+ type Reducer,
6
+ } from 'react';
7
+ import {
8
+ generateSeedPhrase,
9
+ get32ByteSeedFromMnemonic,
10
+ type SeedStrength,
11
+ type Identity,
12
+ } from '@me2em/core';
13
+ import {
14
+ identityFlowReducer,
15
+ initialIdentityFlowState,
16
+ } from '../headless/identity-flow.js';
17
+ import type { IdentityFlowState, IdentityFlowEvent, IdentityFlowStatus } from '../headless/types.js';
18
+ import { useMe2emContext } from './context.js';
19
+
20
+ export interface UseCreateIdentityResult {
21
+ status: IdentityFlowStatus;
22
+ seedWords: string[] | null;
23
+ identity: Identity | null;
24
+ error: string | null;
25
+ generate: (opts?: { strength?: SeedStrength }) => void;
26
+ confirmWords: () => Promise<void>;
27
+ reset: () => void;
28
+ fail: (message: string) => void;
29
+ }
30
+
31
+ export function useCreateIdentity(): UseCreateIdentityResult {
32
+ const { activateIdentity, clearIdentity, identity } = useMe2emContext();
33
+ const [state, dispatch] = useReducer<
34
+ Reducer<IdentityFlowState, IdentityFlowEvent>
35
+ >(identityFlowReducer, initialIdentityFlowState);
36
+
37
+ const generate = useCallback(
38
+ (opts?: { strength?: SeedStrength }) => {
39
+ const words = generateSeedPhrase(opts?.strength ?? 128);
40
+ get32ByteSeedFromMnemonic(words).then((derivedSeed) => {
41
+ dispatch({ type: 'GENERATE', words, seed: derivedSeed });
42
+ }).catch(() => {
43
+ dispatch({ type: 'FAIL', message: 'Failed to generate seed' });
44
+ });
45
+ },
46
+ []
47
+ );
48
+
49
+ const confirmWords = useCallback(async () => {
50
+ if (state.status !== 'seed-generated' || !state.identitySeed) return;
51
+ dispatch({ type: 'CONFIRM_WORDS' });
52
+ const ok = await activateIdentity(state.identitySeed);
53
+ if (!ok) {
54
+ dispatch({ type: 'FAIL', message: 'Identity activation failed' });
55
+ }
56
+ }, [state.status, state.identitySeed, activateIdentity]);
57
+
58
+ const reset = useCallback(() => {
59
+ dispatch({ type: 'RESET' });
60
+ clearIdentity();
61
+ }, [clearIdentity]);
62
+
63
+ const fail = useCallback((message: string) => {
64
+ dispatch({ type: 'FAIL', message });
65
+ }, []);
66
+
67
+ return {
68
+ status: state.status,
69
+ seedWords: state.seedWords,
70
+ identity,
71
+ error: state.error,
72
+ generate,
73
+ confirmWords,
74
+ reset,
75
+ fail,
76
+ };
77
+ }
@@ -0,0 +1,27 @@
1
+ // packages/react/src/react/useHandle.ts
2
+ import { useState, useEffect } from 'react';
3
+ import { type Handle } from '@me2em/core';
4
+ import { useMe2emContext } from './context.js';
5
+
6
+ export function useHandle(name: string): Handle | null {
7
+ const { identity } = useMe2emContext();
8
+ const [handle, setHandle] = useState<Handle | null>(null);
9
+
10
+ useEffect(() => {
11
+ let cancelled = false;
12
+ if (!identity) {
13
+ setHandle(null);
14
+ return;
15
+ }
16
+ // Core performs full name canonicalization (NFKC, lowercase,
17
+ // trim) and validation — no local normalization here.
18
+ identity.deriveHandle(name).then((h) => {
19
+ if (!cancelled) setHandle(h);
20
+ }).catch((err) => {
21
+ if (!cancelled) console.error('useHandle derivation failed', err);
22
+ });
23
+ return () => { cancelled = true; };
24
+ }, [identity, name]);
25
+
26
+ return handle;
27
+ }
@@ -0,0 +1,88 @@
1
+ // packages/react/src/react/useIdentityContext.ts
2
+ import { useState, useCallback, useEffect, useRef } from 'react';
3
+ import type { IdentityContextStorage, IdentityContextData } from '../headless/identity-context-types.js';
4
+ import { useMe2emContext } from './context.js';
5
+
6
+ function publicKeyToId(publicKey: Uint8Array): string {
7
+ return Array.from(publicKey)
8
+ .map((b) => b.toString(16).padStart(2, '0'))
9
+ .join('');
10
+ }
11
+
12
+ export interface UseIdentityContextResult {
13
+ context: IdentityContextData | null;
14
+ isLoaded: boolean;
15
+ error: string | null;
16
+ save: (data: IdentityContextData) => Promise<void>;
17
+ clear: () => Promise<void>;
18
+ /** Reload the cache for the current identity (after sync). */
19
+ refresh: () => Promise<void>;
20
+ }
21
+
22
+ export function useIdentityContext(
23
+ storage: IdentityContextStorage
24
+ ): UseIdentityContextResult {
25
+ const { identity } = useMe2emContext();
26
+ const [context, setContext] = useState<IdentityContextData | null>(null);
27
+ const [isLoaded, setIsLoaded] = useState(false);
28
+ const [error, setError] = useState<string | null>(null);
29
+
30
+ const prevIdentityIdRef = useRef<string | null>(null);
31
+
32
+ useEffect(() => {
33
+ let cancelled = false;
34
+
35
+ if (!identity) {
36
+ setContext(null);
37
+ setIsLoaded(true);
38
+ setError(null);
39
+ return;
40
+ }
41
+
42
+ const identityId = publicKeyToId(identity.getPublicKey());
43
+
44
+ // If identity changed, close old storage and open new
45
+ if (prevIdentityIdRef.current !== null && prevIdentityIdRef.current !== identityId) {
46
+ storage.close().catch(() => { /* ignore close errors */ });
47
+ }
48
+
49
+ prevIdentityIdRef.current = identityId;
50
+
51
+ // Open storage for this identity and load cached context (atomic)
52
+ storage.openAndLoad(identityId)
53
+ .then((data) => {
54
+ if (cancelled) return;
55
+ setContext(data);
56
+ setIsLoaded(true);
57
+ setError(null);
58
+ })
59
+ .catch((err) => {
60
+ if (cancelled) return;
61
+ setError(err instanceof Error ? err.message : String(err));
62
+ setIsLoaded(true);
63
+ });
64
+
65
+ return () => {
66
+ cancelled = true;
67
+ };
68
+ }, [identity, storage]);
69
+
70
+ const save = useCallback(async (data: IdentityContextData): Promise<void> => {
71
+ await storage.save(data);
72
+ setContext(data);
73
+ }, [storage]);
74
+
75
+ const clear = useCallback(async (): Promise<void> => {
76
+ await storage.clear();
77
+ setContext(null);
78
+ }, [storage]);
79
+
80
+ const refresh = useCallback(async (): Promise<void> => {
81
+ if (!identity) return;
82
+ const identityId = publicKeyToId(identity.getPublicKey());
83
+ const data = await storage.openAndLoad(identityId);
84
+ setContext(data);
85
+ }, [identity, storage]);
86
+
87
+ return { context, isLoaded, error, save, clear, refresh };
88
+ }
@@ -0,0 +1,60 @@
1
+ // packages/react/src/react/useImportSeed.ts
2
+ import { useReducer, useCallback, type Reducer } from 'react';
3
+ import {
4
+ validateSeedPhrase,
5
+ get32ByteSeedFromMnemonic,
6
+ type Identity,
7
+ } from '@me2em/core';
8
+ import {
9
+ identityFlowReducer,
10
+ initialIdentityFlowState,
11
+ } from '../headless/identity-flow.js';
12
+ import type { IdentityFlowState, IdentityFlowEvent, IdentityFlowStatus } from '../headless/types.js';
13
+ import { useMe2emContext } from './context.js';
14
+
15
+ export interface UseImportSeedResult {
16
+ status: IdentityFlowStatus;
17
+ identity: Identity | null;
18
+ error: string | null;
19
+ importWords: (words: string[], passphrase?: string) => Promise<void>;
20
+ reset: () => void;
21
+ }
22
+
23
+ export function useImportSeed(): UseImportSeedResult {
24
+ const { activateIdentity, clearIdentity, identity } = useMe2emContext();
25
+ const [state, dispatch] = useReducer<
26
+ Reducer<IdentityFlowState, IdentityFlowEvent>
27
+ >(identityFlowReducer, initialIdentityFlowState);
28
+
29
+ const importWords = useCallback(
30
+ async (words: string[], passphrase?: string) => {
31
+ const validation = validateSeedPhrase(words);
32
+ if (!validation.isValid) {
33
+ dispatch({ type: 'FAIL', message: validation.error ?? 'Invalid seed phrase' });
34
+ return;
35
+ }
36
+ try {
37
+ const seed = await get32ByteSeedFromMnemonic(words, passphrase ?? '');
38
+ dispatch({ type: 'IMPORT', seed });
39
+ await activateIdentity(seed);
40
+ } catch (err) {
41
+ const message = err instanceof Error ? err.message : 'Import failed';
42
+ dispatch({ type: 'FAIL', message });
43
+ }
44
+ },
45
+ [activateIdentity]
46
+ );
47
+
48
+ const reset = useCallback(() => {
49
+ dispatch({ type: 'RESET' });
50
+ clearIdentity();
51
+ }, [clearIdentity]);
52
+
53
+ return {
54
+ status: state.status,
55
+ identity,
56
+ error: state.error,
57
+ importWords,
58
+ reset,
59
+ };
60
+ }