diff --git a/extension/src/popup/components/setup-wizard.ts b/extension/src/popup/components/setup-wizard.ts index 9d1f887..d604a9c 100644 --- a/extension/src/popup/components/setup-wizard.ts +++ b/extension/src/popup/components/setup-wizard.ts @@ -1,271 +1,30 @@ -/// Setup wizard — 3-step flow: host config, image upload, test unlock. +/// Setup prompt — directs users to the full-page setup wizard. +/// +/// The popup is too constrained for file pickers and multi-step forms +/// (Chrome closes it when focus shifts). All real setup happens in +/// setup.html, which pushes config to chrome.storage.local when done. -import { getState, setState, sendMessage, navigate, escapeHtml } from '../popup'; -import type { VaultConfig, ManifestEntry } from '../../shared/types'; - -let wizardStep = 0; -let wizardConfig: Partial = { - hostType: 'gitea', -}; -let wizardImageBase64: string | null = null; +import { escapeHtml } from '../popup'; export function renderSetupWizard(app: HTMLElement): void { - // Check if image is already in storage (e.g. pushed by init wizard). - if (!wizardImageBase64) { - chrome.storage.local.get(['imageBase64'], (result: Record) => { - if (result.imageBase64) { - wizardImageBase64 = result.imageBase64; - // Re-render now that we have the image. - renderSetupWizard(app); - } - }); - } - - const state = getState(); - - // Progress bar. - const progressHtml = ` -
-
-
-
-
- `; - - let stepHtml = ''; - - switch (wizardStep) { - case 0: - stepHtml = renderStep0(); - break; - case 1: - stepHtml = renderStep1(); - break; - case 2: - stepHtml = renderStep2(state); - break; - } - app.innerHTML = ` -
-
idfoto setup
- ${progressHtml} - ${stepHtml} -
- `; +
+
idfoto
+

two-factor vault

- // Attach event listeners after rendering. - switch (wizardStep) { - case 0: attachStep0Listeners(); break; - case 1: attachStep1Listeners(); break; - case 2: attachStep2Listeners(); break; - } -} - -// --- Step 0: Host configuration --- - -function renderStep0(): string { - return ` -
-

git host

-
- -
- - -
-
-
- - -
-
- - -
-
- - -
-
- -
-
- `; -} - -function attachStep0Listeners(): void { - // Host type toggle. - document.querySelectorAll('.toggle-group button').forEach(btn => { - btn.addEventListener('click', () => { - const hostType = (btn as HTMLElement).dataset.host as 'gitea' | 'github'; - wizardConfig.hostType = hostType; - const urlGroup = document.getElementById('host-url-group'); - if (urlGroup) { - urlGroup.style.display = hostType === 'github' ? 'none' : ''; - } - document.querySelectorAll('.toggle-group button').forEach(b => b.classList.remove('active')); - btn.classList.add('active'); - }); - }); - - document.getElementById('next-btn')?.addEventListener('click', () => { - const repoPath = (document.getElementById('repo-path') as HTMLInputElement).value.trim(); - const apiToken = (document.getElementById('api-token') as HTMLInputElement).value.trim(); - const hostUrl = wizardConfig.hostType === 'github' - ? 'https://api.github.com' - : (document.getElementById('host-url') as HTMLInputElement).value.trim(); - - if (!repoPath || !apiToken) { - setState({ error: 'Repository and API token are required' }); - return; - } - if (wizardConfig.hostType === 'gitea' && !hostUrl) { - setState({ error: 'Host URL is required for Gitea' }); - return; - } - - wizardConfig = { ...wizardConfig, hostUrl, repoPath, apiToken }; - wizardStep = 1; - setState({ error: null }); - }); -} - -// --- Step 1: Reference image --- -// -// Chrome closes the popup when a file picker steals focus, which makes -// in-popup file uploads unreliable. Instead we check if an image is -// already in chrome.storage.local (pushed by the init wizard or a -// previous setup). If not, we link the user to the full-page setup.html -// where the file picker works without focus issues. - -function renderStep1(): string { - if (wizardImageBase64) { - // Image already loaded (from storage or previous attempt). - return ` -
-

reference image

-

✓ reference image loaded

-
- - -
-
- `; - } - - return ` -
-

reference image

-

- No reference image found. Use the full-page setup wizard to - upload your reference image — file pickers don't work reliably - in the popup. +

+ No vault configured yet. Open the setup wizard to + create a new vault or connect to an existing one.

-
- - -
+ +
`; -} - -function attachStep1Listeners(): void { - document.getElementById('back-btn')?.addEventListener('click', () => { - wizardStep = 0; - setState({ error: null }); - }); - - document.getElementById('next-btn')?.addEventListener('click', () => { - if (!wizardImageBase64) return; - wizardStep = 2; - setState({ error: null }); - }); document.getElementById('open-setup-btn')?.addEventListener('click', () => { - // Open the full-page setup wizard in a new tab. chrome.tabs.create({ url: chrome.runtime.getURL('setup.html') }); window.close(); }); } - -// --- Step 2: Test unlock --- - -function renderStep2(state: ReturnType): string { - return ` -
-

test unlock

-

- Enter your passphrase to verify the configuration works. -

-
- -
- ${state.loading ? '
' : ''} - ${state.error ? `
${escapeHtml(state.error)}
` : ''} -
- - -
-
- `; -} - -function attachStep2Listeners(): void { - document.getElementById('back-btn')?.addEventListener('click', () => { - wizardStep = 1; - setState({ error: null }); - }); - - const saveBtn = document.getElementById('save-btn'); - const input = document.getElementById('test-passphrase') as HTMLInputElement; - - const doSave = async () => { - const passphrase = input?.value; - if (!passphrase) { - setState({ error: 'Passphrase is required' }); - return; - } - - setState({ loading: true, error: null }); - - // Save config first. - const saveResp = await sendMessage({ - type: 'save_setup', - config: wizardConfig as VaultConfig, - imageBase64: wizardImageBase64!, - }); - - if (!saveResp.ok) { - setState({ loading: false, error: saveResp.error }); - return; - } - - // Try to unlock. - const unlockResp = await sendMessage({ type: 'unlock', passphrase }); - if (!unlockResp.ok) { - setState({ loading: false, error: unlockResp.error }); - return; - } - - // Success — go to entry list. - const listResp = await sendMessage({ type: 'list_entries' }); - if (listResp.ok) { - const data = listResp.data as { entries: Array<[string, ManifestEntry]> }; - // Reset wizard state for next time. - wizardStep = 0; - wizardConfig = { hostType: 'gitea' }; - wizardImageBase64 = null; - navigate('list', { entries: data.entries }); - } else { - setState({ loading: false, error: listResp.error }); - } - }; - - saveBtn?.addEventListener('click', doSave); - input?.addEventListener('keydown', (e) => { - if (e.key === 'Enter') doSave(); - }); - - input?.focus(); -}