diff --git a/src/encryptid/ui/login-button.ts b/src/encryptid/ui/login-button.ts
index 1e987fc..67451bf 100644
--- a/src/encryptid/ui/login-button.ts
+++ b/src/encryptid/ui/login-button.ts
@@ -360,6 +360,41 @@ const styles = `
color: var(--eid-text-secondary);
opacity: 0.7;
}
+
+ .email-fallback-link {
+ display: block;
+ margin-top: 10px;
+ padding: 0;
+ background: none;
+ border: none;
+ color: var(--eid-text-secondary);
+ font-size: 0.8rem;
+ cursor: pointer;
+ text-align: center;
+ width: 100%;
+ font-family: inherit;
+ }
+
+ .email-fallback-link:hover {
+ color: var(--eid-primary);
+ text-decoration: underline;
+ }
+
+ .email-fallback-section {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-top: 10px;
+ padding-top: 10px;
+ border-top: 1px solid rgba(255,255,255,0.1);
+ }
+
+ .email-sent-msg {
+ font-size: 0.85rem;
+ color: #22c55e;
+ text-align: center;
+ padding: 8px;
+ }
`;
// ============================================================================
@@ -383,6 +418,8 @@ export class EncryptIDLoginButton extends HTMLElement {
private shadow: ShadowRoot;
private loading: boolean = false;
private showDropdown: boolean = false;
+ private showEmailFallback: boolean = false;
+ private emailSent: boolean = false;
private capabilities: WebAuthnCapabilities | null = null;
// Configurable attributes
@@ -484,15 +521,23 @@ export class EncryptIDLoginButton extends HTMLElement {
const accounts = getKnownAccounts();
- // No known accounts → username input + sign-in button
+ // No known accounts → passkey-first button + email fallback
if (accounts.length === 0) {
+ const emailSection = this.showEmailFallback ? (this.emailSent
+ ? `
Login link sent! Check your inbox.
`
+ : `
+
+
+
`) : '';
+
return `
-
-
`;
}
@@ -585,17 +630,27 @@ export class EncryptIDLoginButton extends HTMLElement {
});
});
} else {
- // Username input form (no known accounts)
- const usernameInput = this.shadow.querySelector('.username-input') as HTMLInputElement;
- const usernameLoginBtn = this.shadow.querySelector('[data-action="username-login"]');
- if (usernameInput && usernameLoginBtn) {
- const doLogin = () => {
- const val = usernameInput.value.trim();
- this.handleLogin(val || undefined);
- };
- usernameLoginBtn.addEventListener('click', doLogin);
- usernameInput.addEventListener('keydown', (e) => {
- if ((e as KeyboardEvent).key === 'Enter') doLogin();
+ // Passkey-first button (no known accounts → unscoped auth)
+ this.shadow.querySelector('[data-action="passkey-login"]')?.addEventListener('click', () => {
+ this.handleLogin();
+ });
+
+ // "No passkey? Sign in with email" toggle
+ this.shadow.querySelector('[data-action="show-email"]')?.addEventListener('click', () => {
+ this.showEmailFallback = true;
+ this.render();
+ });
+
+ // Send magic link button
+ this.shadow.querySelector('[data-action="send-magic-link"]')?.addEventListener('click', () => {
+ this.sendMagicLink();
+ });
+
+ // Email input enter key
+ const emailInput = this.shadow.querySelector('[data-email-input]') as HTMLInputElement;
+ if (emailInput) {
+ emailInput.addEventListener('keydown', (e) => {
+ if ((e as KeyboardEvent).key === 'Enter') this.sendMagicLink();
});
}
@@ -707,8 +762,9 @@ export class EncryptIDLoginButton extends HTMLElement {
}));
} catch (error: any) {
- // If no credential found, offer to register
+ // If no credential found, auto-show email fallback + dispatch event
if (error.name === 'NotAllowedError' || error.message?.includes('No credential')) {
+ this.showEmailFallback = true;
this.dispatchEvent(new CustomEvent('login-register-needed', {
bubbles: true,
}));
@@ -724,6 +780,26 @@ export class EncryptIDLoginButton extends HTMLElement {
}
}
+ private async sendMagicLink() {
+ const emailInput = this.shadow.querySelector('[data-email-input]') as HTMLInputElement;
+ const email = emailInput?.value.trim();
+ if (!email || !email.includes('@')) {
+ emailInput?.focus();
+ return;
+ }
+ try {
+ await fetch(`${ENCRYPTID_AUTH}/api/auth/magic-link`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email }),
+ });
+ this.emailSent = true;
+ this.render();
+ } catch (error: any) {
+ console.error('Magic link failed:', error);
+ }
+ }
+
private async handleDropdownAction(action: string) {
this.showDropdown = false;