refactor: replace popup setup wizard with link to setup.html

The popup is too constrained for multi-step setup (file pickers
close it, fields duplicate the init wizard). Now it just shows
a single button that opens the full-page setup wizard.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-12 11:55:07 -04:00
parent 4c26b4c534
commit c50285c4a5

View File

@@ -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<VaultConfig> = {
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<string, string>) => {
if (result.imageBase64) {
wizardImageBase64 = result.imageBase64;
// Re-render now that we have the image.
renderSetupWizard(app);
}
});
}
const state = getState();
// Progress bar.
const progressHtml = `
<div class="progress-bar">
<div class="step ${wizardStep > 0 ? 'done' : wizardStep === 0 ? 'current' : ''}"></div>
<div class="step ${wizardStep > 1 ? 'done' : wizardStep === 1 ? 'current' : ''}"></div>
<div class="step ${wizardStep > 2 ? 'done' : wizardStep === 2 ? 'current' : ''}"></div>
</div>
`;
let stepHtml = '';
switch (wizardStep) {
case 0:
stepHtml = renderStep0();
break;
case 1:
stepHtml = renderStep1();
break;
case 2:
stepHtml = renderStep2(state);
break;
}
app.innerHTML = `
<div class="pad" style="padding-top:12px;">
<div class="brand" style="margin-bottom:4px;">idfoto setup</div>
${progressHtml}
${stepHtml}
</div>
`;
<div class="pad" style="padding-top:24px;text-align:center;">
<div class="brand" style="font-size:16px;margin-bottom:4px;">idfoto</div>
<p class="secondary" style="margin-bottom:20px;">two-factor vault</p>
// 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 `
<div class="wizard-step">
<h3>git host</h3>
<div class="form-group">
<label class="label">host type</label>
<div class="toggle-group">
<button class="${wizardConfig.hostType === 'gitea' ? 'active' : ''}" data-host="gitea">Gitea</button>
<button class="${wizardConfig.hostType === 'github' ? 'active' : ''}" data-host="github">GitHub</button>
</div>
</div>
<div class="form-group" id="host-url-group" ${wizardConfig.hostType === 'github' ? 'style="display:none;"' : ''}>
<label class="label" for="host-url">host url</label>
<input id="host-url" type="text" value="${escapeHtml(wizardConfig.hostUrl ?? '')}" placeholder="https://git.example.com">
</div>
<div class="form-group">
<label class="label" for="repo-path">repository</label>
<input id="repo-path" type="text" value="${escapeHtml(wizardConfig.repoPath ?? '')}" placeholder="user/vault">
</div>
<div class="form-group">
<label class="label" for="api-token">api token</label>
<input id="api-token" type="password" value="${escapeHtml(wizardConfig.apiToken ?? '')}" placeholder="token with repo read/write access">
</div>
<div class="form-actions">
<button class="btn btn-primary" id="next-btn">next</button>
</div>
</div>
`;
}
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 `
<div class="wizard-step">
<h3>reference image</h3>
<p class="secondary" style="margin-bottom:12px;">✓ reference image loaded</p>
<div class="form-actions" style="margin-top:16px;">
<button class="btn" id="back-btn">back</button>
<button class="btn btn-primary" id="next-btn">next</button>
</div>
</div>
`;
}
return `
<div class="wizard-step">
<h3>reference image</h3>
<p class="muted" style="margin-bottom:12px;">
No reference image found. Use the full-page setup wizard to
upload your reference image — file pickers don't work reliably
in the popup.
<p class="muted" style="margin-bottom:16px;font-size:11px;line-height:1.6;">
No vault configured yet. Open the setup wizard to
create a new vault or connect to an existing one.
</p>
<div class="form-actions" style="margin-top:16px;">
<button class="btn" id="back-btn">back</button>
<button class="btn btn-primary" id="open-setup-btn">open setup page</button>
</div>
<button class="btn btn-primary" id="open-setup-btn" style="width:100%;">
open setup wizard
</button>
</div>
`;
}
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<typeof getState>): string {
return `
<div class="wizard-step">
<h3>test unlock</h3>
<p class="muted" style="margin-bottom:12px;">
Enter your passphrase to verify the configuration works.
</p>
<div class="form-group">
<input id="test-passphrase" type="password" placeholder="passphrase" ${state.loading ? 'disabled' : ''}>
</div>
${state.loading ? '<div style="margin:12px 0;"><span class="spinner"></span></div>' : ''}
${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
<div class="form-actions">
<button class="btn" id="back-btn">back</button>
<button class="btn btn-primary" id="save-btn" ${state.loading ? 'disabled' : ''}>save & unlock</button>
</div>
</div>
`;
}
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();
}