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:
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user