@passkey-fas/element
Version:
FaS Element - Web component cung cấp UI tiếng Việt đẹp cho xác thực passkey, dễ dàng tích hợp vào website
1,051 lines (920 loc) • 31.6 kB
JavaScript
import FaSSDK from '@passkey-fas/webauthn-sdk';
/**
* FaS Element - Web Component for Passkey Authentication
* Based on FaS Platform and using existing SDK
*/
class FaSElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
// Internal state
this.loading = false;
this.currentStep = 'login'; // 'login', 'register', 'email-verification', 'passkey-creation', 'success', 'error'
this.authData = null;
this.sdk = null;
this.userEmail = ''; // Store email for passkey creation
this.emailVerified = false; // Track email verification status
// Configuration from attributes
this.apiUrl = '';
this.clientId = '';
this.useProxy = true; // Default to proxy mode for security
// Bind methods
this.handleLogin = this.handleLogin.bind(this);
this.handleRegister = this.handleRegister.bind(this);
this.handleEmailVerification = this.handleEmailVerification.bind(this);
this.handleResendCode = this.handleResendCode.bind(this);
this.handleQuickLogin = this.handleQuickLogin.bind(this);
this.handleCreatePasskey = this.handleCreatePasskey.bind(this);
this.handleBack = this.handleBack.bind(this);
this.handleSkip = this.handleSkip.bind(this);
this.switchToRegister = this.switchToRegister.bind(this);
this.switchToLogin = this.switchToLogin.bind(this);
}
// Observed attributes
static get observedAttributes() {
return ['api-url', 'project-id', 'theme', 'lang'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
switch (name) {
case 'api-url':
this.apiUrl = newValue || '/api/public';
break;
case 'project-id':
this.projectId = newValue;
break;
}
this.initializeSDK();
this.render();
}
}
connectedCallback() {
// Set default values
this.apiUrl = this.getAttribute('api-url') || '/api/public';
this.projectId = this.getAttribute('project-id') || '';
this.initializeSDK();
this.render();
}
// Initialize SDK with current configuration
initializeSDK() {
if (this.projectId) {
this.sdk = new FaSSDK({
projectId: this.projectId,
apiBase: this.apiUrl,
useProxy: false // Always direct call to public API
});
}
}
// Material-UI inspired styles
getStyles() {
return `
<style>
:host {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
--primary-color: #1976d2;
--primary-hover: #1565c0;
--secondary-color: #dc004e;
--success-color: #2e7d32;
--error-color: #d32f2f;
--warning-color: #ed6c02;
--text-primary: rgba(0, 0, 0, 0.87);
--text-secondary: rgba(0, 0, 0, 0.6);
--border-color: rgba(0, 0, 0, 0.23);
--background: #ffffff;
--surface: #f5f5f5;
}
.fas-container {
background: var(--background);
border-radius: 4px;
padding: 24px;
max-width: 400px;
width: 100%;
box-shadow: 0px 2px 1px -1px rgba(0,0,0,0.2),
0px 1px 1px 0px rgba(0,0,0,0.14),
0px 1px 3px 0px rgba(0,0,0,0.12);
border: 1px solid var(--border-color);
}
.fas-header {
text-align: center;
margin-bottom: 32px;
}
.fas-title {
font-size: 1.5rem;
font-weight: 400;
color: var(--text-primary);
margin: 0 0 8px 0;
line-height: 1.334;
}
.fas-subtitle {
font-size: 0.875rem;
color: var(--text-secondary);
margin: 0;
line-height: 1.43;
}
.fas-form {
display: flex;
flex-direction: column;
gap: 24px;
}
.fas-input-group {
display: flex;
flex-direction: column;
position: relative;
}
.fas-label {
font-size: 0.75rem;
font-weight: 400;
color: var(--text-secondary);
margin-bottom: 4px;
text-transform: uppercase;
letter-spacing: 0.00938em;
}
.fas-input {
font-size: 1rem;
font-weight: 400;
line-height: 1.4375em;
color: var(--text-primary);
box-sizing: border-box;
position: relative;
background: none;
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 12px 16px;
outline: none;
transition: border-color 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
}
.fas-input:focus {
border-color: var(--primary-color);
border-width: 2px;
padding: 11px 15px;
}
.fas-input:hover:not(:focus) {
border-color: var(--text-primary);
}
.fas-button {
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
box-sizing: border-box;
background-color: transparent;
outline: 0;
border: 0;
margin: 0;
border-radius: 4px;
padding: 6px 16px;
cursor: pointer;
user-select: none;
vertical-align: middle;
text-decoration: none;
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
font-weight: 500;
font-size: 0.875rem;
line-height: 1.75;
letter-spacing: 0.02857em;
text-transform: uppercase;
min-width: 64px;
min-height: 36px;
transition: background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,
box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,
border-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,
color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
gap: 8px;
}
.fas-button:disabled {
color: rgba(0, 0, 0, 0.26);
box-shadow: none;
background-color: rgba(0, 0, 0, 0.12);
cursor: default;
pointer-events: none;
}
.fas-button-contained {
color: #fff;
background-color: var(--primary-color);
box-shadow: 0px 3px 1px -2px rgba(0,0,0,0.2),
0px 2px 2px 0px rgba(0,0,0,0.14),
0px 1px 5px 0px rgba(0,0,0,0.12);
}
.fas-button-contained:hover:not(:disabled) {
background-color: var(--primary-hover);
box-shadow: 0px 2px 4px -1px rgba(0,0,0,0.2),
0px 4px 5px 0px rgba(0,0,0,0.14),
0px 1px 10px 0px rgba(0,0,0,0.12);
}
.fas-button-outlined {
color: var(--primary-color);
border: 1px solid rgba(25, 118, 210, 0.5);
}
.fas-button-outlined:hover:not(:disabled) {
border: 1px solid var(--primary-color);
background-color: rgba(25, 118, 210, 0.04);
}
.fas-button-text {
color: var(--primary-color);
}
.fas-button-text:hover:not(:disabled) {
background-color: rgba(25, 118, 210, 0.04);
}
.fas-circular-progress {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: fas-spin 1s linear infinite;
}
fas-spin {
to { transform: rotate(360deg); }
}
/* Responsive design */
(max-width: 480px) {
:host {
padding: 16px;
}
.fas-container {
max-width: 100%;
padding: 20px;
}
}
.fas-alert {
padding: 6px 16px;
font-size: 0.875rem;
font-weight: 400;
line-height: 1.43;
border-radius: 4px;
letter-spacing: 0.01071em;
display: flex;
margin: 16px 0;
}
.fas-alert-success {
color: rgb(30, 70, 32);
background-color: rgb(237, 247, 237);
border: 1px solid rgb(200, 230, 201);
}
.fas-alert-error {
color: rgb(95, 33, 32);
background-color: rgb(253, 237, 237);
border: 1px solid rgb(244, 199, 199);
}
.fas-alert-warning {
color: rgb(102, 60, 0);
background-color: rgb(255, 244, 229);
border: 1px solid rgb(255, 220, 177);
}
.fas-alert-info {
color: rgb(13, 60, 97);
background-color: rgb(229, 246, 253);
border: 1px solid rgb(179, 229, 252);
}
.fas-divider {
display: flex;
align-items: center;
margin: 24px 0;
color: var(--text-secondary);
}
.fas-divider::before,
.fas-divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--border-color);
}
.fas-divider::before {
margin-right: 16px;
}
.fas-divider::after {
margin-left: 16px;
}
.fas-divider span {
margin: 0 16px;
font-size: 0.875rem;
}
.fas-link {
color: var(--primary-color);
text-decoration: none;
font-size: 0.875rem;
font-weight: 500;
}
.fas-link:hover {
text-decoration: underline;
}
.fas-center {
text-align: center;
margin-top: 16px;
}
.fas-info-card {
background: rgba(13, 60, 97, 0.04);
border: 1px solid rgba(13, 60, 97, 0.12);
border-radius: 4px;
padding: 16px;
margin-bottom: 24px;
font-size: 0.875rem;
color: rgb(13, 60, 97);
}
.fas-info-card strong {
color: rgb(13, 60, 97);
font-weight: 500;
}
/* Responsive */
(max-width: 600px) {
.fas-container {
margin: 16px;
padding: 16px;
}
}
.fas-button-passkey {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.fas-passkey-icon {
font-size: 1.2em;
}
.fas-footer {
text-align: center;
margin-top: 24px;
}
.fas-footer-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
.fas-link {
background: none;
border: none;
color: var(--primary-color);
text-decoration: none;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
padding: 4px 8px;
border-radius: 4px;
transition: background-color 200ms;
}
.fas-link:hover {
background-color: rgba(25, 118, 210, 0.04);
}
</style>
`;
}
// Template for login step
getLoginTemplate() {
return `
<div class="fas-container">
<div class="fas-header">
<h2 class="fas-title">Đăng nhập</h2>
</div>
<form class="fas-form" id="loginForm">
<div class="fas-input-group">
<input
class="fas-input"
type="email"
id="email"
placeholder="Email"
required
${this.loading ? 'disabled' : ''}
/>
</div>
<button
type="submit"
class="fas-button fas-button-contained"
${this.loading ? 'disabled' : ''}
>
${this.loading ? 'Đang xử lý...' : 'Tiếp tục'}
</button>
<div class="fas-divider">
<span>hoặc</span>
</div>
<button
type="button"
class="fas-button fas-button-outlined fas-button-passkey"
id="quickLoginBtn"
${this.loading ? 'disabled' : ''}
>
<span class="fas-passkey-icon">🔑</span>
Đăng nhập bằng passkey
</button>
</form>
<div class="fas-footer">
<button type="button" class="fas-link" id="switchToRegister">
Không có tài khoản?
</button>
</div>
</div>
`;
}
// Template for register step
getRegisterTemplate() {
return `
<div class="fas-container">
<div class="fas-header">
<h2 class="fas-title">Tạo tài khoản</h2>
</div>
<form class="fas-form" id="registerForm">
<div class="fas-input-group">
<input
class="fas-input"
type="email"
id="email"
placeholder="Email"
required
${this.loading ? 'disabled' : ''}
/>
</div>
<button
type="submit"
class="fas-button fas-button-contained"
${this.loading ? 'disabled' : ''}
>
${this.loading ? 'Đang xử lý...' : 'Tiếp tục'}
</button>
</form>
<div class="fas-footer">
<button type="button" class="fas-link" id="switchToLogin">
Đã có tài khoản?
</button>
</div>
</div>
`;
}
// Template for email verification step
getEmailVerificationTemplate() {
return `
<div class="fas-container">
<div class="fas-header">
<h2 class="fas-title">Xác thực email</h2>
<p class="fas-subtitle">
Chúng tôi đã gửi mã xác thực 6 chữ số đến email <strong>${this.userEmail}</strong>.
Vui lòng nhập mã để tiếp tục.
</p>
</div>
<form class="fas-form" id="verificationForm">
<div class="fas-input-group">
<input
class="fas-input"
type="text"
id="passcode"
placeholder="Nhập mã xác thực"
maxlength="6"
pattern="[0-9]{6}"
required
${this.loading ? 'disabled' : ''}
style="text-align: center; font-size: 1.2rem; letter-spacing: 2px;"
/>
</div>
<button
type="submit"
class="fas-button fas-button-contained"
${this.loading ? 'disabled' : ''}
>
${this.loading ? 'Đang xác thực...' : 'Xác thực'}
</button>
</form>
<div class="fas-footer fas-footer-actions">
<button type="button" class="fas-link" id="backBtn">
Quay lại
</button>
<button type="button" class="fas-link" id="resendCodeBtn" ${this.loading ? 'disabled' : ''}>
Gửi lại mã
</button>
</div>
</div>
`;
}
// Template for passkey creation step
getPasskeyCreationTemplate() {
return `
<div class="fas-container">
<div class="fas-header">
<h2 class="fas-title">Tạo passkey</h2>
<p class="fas-subtitle">
Đăng nhập vào tài khoản của bạn một cách dễ dàng và bảo mật với passkey.
Lưu ý: Dữ liệu sinh trắc học của bạn chỉ được lưu trữ trên thiết bị
và sẽ không bao giờ được chia sẻ với bất kỳ ai.
</p>
</div>
<div class="fas-form">
<button
type="button"
class="fas-button fas-button-contained fas-button-passkey"
id="createPasskeyBtn"
${this.loading ? 'disabled' : ''}
>
<span class="fas-passkey-icon">🔑</span>
${this.loading ? 'Đang tạo passkey...' : 'Tạo passkey'}
</button>
</div>
<div class="fas-footer fas-footer-actions">
<button type="button" class="fas-link" id="backBtn">
Quay lại
</button>
<button type="button" class="fas-link" id="skipBtn">
Bỏ qua
</button>
</div>
</div>
`;
}
// Template for success step
getSuccessTemplate() {
const user = this.authData?.user;
return `
<div class="fas-container">
<div class="fas-header">
<h2 class="fas-title">Thành công!</h2>
<p class="fas-subtitle">Bạn đã ${this.currentStep === 'register-success' ? 'đăng ký' : 'đăng nhập'} thành công</p>
</div>
${user ? `
<div class="fas-alert fas-alert-success">
<strong>Chào mừng, ${user.fullname || user.email}!</strong><br>
ID: ${user.id}<br>
Email: ${user.email}<br>
${user.lastLogin ? `Đăng nhập lần cuối: ${new Date(user.lastLogin).toLocaleString('vi-VN')}` : ''}
</div>
` : ''}
<button
type="button"
class="fas-button fas-button-outlined"
id="resetBtn"
>
🔄 Đăng nhập lại
</button>
</div>
`;
}
// Template for error step
getErrorTemplate(errorMessage) {
return `
<div class="fas-container">
<div class="fas-header">
<h2 class="fas-title">Có lỗi xảy ra</h2>
<p class="fas-subtitle">Vui lòng thử lại</p>
</div>
<div class="fas-alert fas-alert-error">
${errorMessage}
</div>
<button
type="button"
class="fas-button fas-button-contained"
id="retryBtn"
>
🔄 Thử lại
</button>
</div>
`;
}
// Render the component
render() {
let template = '';
switch (this.currentStep) {
case 'register':
template = this.getRegisterTemplate();
break;
case 'email-verification':
template = this.getEmailVerificationTemplate();
break;
case 'passkey-creation':
template = this.getPasskeyCreationTemplate();
break;
case 'success':
case 'register-success':
template = this.getSuccessTemplate();
break;
case 'error':
template = this.getErrorTemplate(this.errorMessage || 'Đã xảy ra lỗi không xác định');
break;
default:
template = this.getLoginTemplate();
break;
}
this.shadowRoot.innerHTML = this.getStyles() + template;
this.attachEventListeners();
}
// Attach event listeners
attachEventListeners() {
const loginForm = this.shadowRoot.getElementById('loginForm');
const registerForm = this.shadowRoot.getElementById('registerForm');
const verificationForm = this.shadowRoot.getElementById('verificationForm');
const switchToRegister = this.shadowRoot.getElementById('switchToRegister');
const switchToLogin = this.shadowRoot.getElementById('switchToLogin');
const quickLoginBtn = this.shadowRoot.getElementById('quickLoginBtn');
const createPasskeyBtn = this.shadowRoot.getElementById('createPasskeyBtn');
const backBtn = this.shadowRoot.getElementById('backBtn');
const skipBtn = this.shadowRoot.getElementById('skipBtn');
const resetBtn = this.shadowRoot.getElementById('resetBtn');
const retryBtn = this.shadowRoot.getElementById('retryBtn');
const resendCodeBtn = this.shadowRoot.getElementById('resendCodeBtn');
if (loginForm) {
loginForm.addEventListener('submit', this.handleLogin);
}
if (registerForm) {
registerForm.addEventListener('submit', this.handleRegister);
}
if (verificationForm) {
verificationForm.addEventListener('submit', this.handleEmailVerification);
}
if (switchToRegister) {
switchToRegister.addEventListener('click', this.switchToRegister);
}
if (switchToLogin) {
switchToLogin.addEventListener('click', this.switchToLogin);
}
if (quickLoginBtn) {
quickLoginBtn.addEventListener('click', this.handleQuickLogin);
}
if (createPasskeyBtn) {
createPasskeyBtn.addEventListener('click', this.handleCreatePasskey);
}
if (backBtn) {
backBtn.addEventListener('click', this.handleBack);
}
if (skipBtn) {
skipBtn.addEventListener('click', this.handleSkip);
}
if (resetBtn) {
resetBtn.addEventListener('click', () => this.reset());
}
if (retryBtn) {
retryBtn.addEventListener('click', () => {
this.currentStep = 'login';
this.render();
});
}
if (resendCodeBtn) {
resendCodeBtn.addEventListener('click', this.handleResendCode);
}
}
// Handle login form submission
async handleLogin(event) {
event.preventDefault();
const email = this.shadowRoot.getElementById('email').value.trim();
if (!email) {
this.showError('Vui lòng nhập email');
return;
}
// Store email and navigate to passkey creation
this.userEmail = email;
this.currentStep = 'passkey-creation';
this.render();
}
// Handle register form submission
async handleRegister(event) {
event.preventDefault();
const email = this.shadowRoot.getElementById('email').value.trim();
if (!email) {
this.showError('Vui lòng nhập email');
return;
}
this.setLoading(true);
this.userEmail = email;
try {
// Send verification code via public API
const response = await fetch(`${this.apiUrl.replace(/\/webauthn$/, '')}/webauthn/send-verification`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Project-ID': this.projectId
},
body: JSON.stringify({ email })
});
const data = await response.json();
if (data.success) {
this.currentStep = 'email-verification';
this.render();
} else {
throw new Error(data.message || 'Không thể gửi mã xác thực');
}
} catch (error) {
console.error('❌ Send verification error:', error);
this.showError(this.getErrorMessage(error));
} finally {
this.setLoading(false);
}
}
// Handle email verification form submission
async handleEmailVerification(event) {
event.preventDefault();
const passcode = this.shadowRoot.getElementById('passcode').value.trim();
if (!passcode) {
this.showError('Vui lòng nhập mã xác thực');
return;
}
if (passcode.length !== 6) {
this.showError('Mã xác thực phải có 6 chữ số');
return;
}
this.setLoading(true);
try {
// Verify passcode via public API
const response = await fetch(`${this.apiUrl.replace(/\/webauthn$/, '')}/webauthn/verify-passcode`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Project-ID': this.projectId
},
body: JSON.stringify({
email: this.userEmail,
passcode
})
});
const data = await response.json();
if (data.success) {
this.emailVerified = true;
this.currentStep = 'passkey-creation';
this.render();
} else {
throw new Error(data.message || 'Mã xác thực không đúng');
}
} catch (error) {
console.error('❌ Email verification error:', error);
this.showError(this.getErrorMessage(error));
} finally {
this.setLoading(false);
}
}
// Handle resend verification code
async handleResendCode() {
this.setLoading(true);
try {
// Send verification code via public API
const response = await fetch(`${this.apiUrl.replace(/\/webauthn$/, '')}/webauthn/send-verification`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Project-ID': this.projectId
},
body: JSON.stringify({ email: this.userEmail })
});
const data = await response.json();
if (data.success) {
// Show success message briefly
this.shadowRoot.querySelector('.fas-subtitle').innerHTML =
`Mã xác thực mới đã được gửi đến email <strong>${this.userEmail}</strong>. Vui lòng kiểm tra hộp thư của bạn.`;
} else {
throw new Error(data.message || 'Không thể gửi lại mã xác thực');
}
} catch (error) {
console.error('❌ Resend code error:', error);
this.showError(this.getErrorMessage(error));
} finally {
this.setLoading(false);
}
}
// Handle quick login with passkey
async handleQuickLogin() {
if (!this.sdk) {
this.showError('SDK chưa được khởi tạo. Vui lòng kiểm tra client-id.');
return;
}
// Nếu chưa có email đã lưu, cố gắng lấy từ ô input hiện tại
if (!this.userEmail) {
const emailInput = this.shadowRoot.querySelector('input[type="email"]');
if (emailInput && emailInput.value) {
this.userEmail = emailInput.value.trim();
}
}
if (!this.userEmail) {
this.showError('Vui lòng nhập email trước khi đăng nhập.');
return;
}
this.setLoading(true);
try {
console.log('🔐 Starting quick passkey authentication for', this.userEmail);
const result = await this.sdk.authenticatePasskey(this.userEmail);
this.authData = result;
this.currentStep = 'success';
this.dispatchEvent(new CustomEvent('fas-success', {
detail: { type: 'quick-login', user: result.user, token: result.token }
}));
} catch (error) {
console.error('❌ Quick login failed:', error);
this.showError(this.getErrorMessage(error));
} finally {
this.setLoading(false);
}
}
// Handle create passkey
async handleCreatePasskey() {
if (!this.sdk) {
this.showError('SDK chưa được khởi tạo. Vui lòng kiểm tra client-id.');
return;
}
if (!this.userEmail) {
this.showError('Email không được tìm thấy. Vui lòng thử lại.');
return;
}
this.setLoading(true);
try {
let result;
// Try to authenticate first (user might already have a passkey)
try {
console.log('🔐 Trying to authenticate existing passkey...');
result = await this.sdk.authenticatePasskey(this.userEmail);
console.log('✅ Authentication successful');
} catch (authError) {
// If authentication fails, try to register new passkey
console.log('🔑 Authentication failed, trying to register new passkey...');
result = await this.sdk.registerPasskey(this.userEmail, this.userEmail);
console.log('✅ Registration successful');
}
this.authData = result;
this.currentStep = 'success';
this.dispatchEvent(new CustomEvent('fas-success', {
detail: { type: 'passkey-creation', user: result.user, token: result.token }
}));
} catch (error) {
console.error('❌ Passkey creation/authentication failed:', error);
this.showError(this.getErrorMessage(error));
} finally {
this.setLoading(false);
}
}
// Handle back
handleBack() {
if (this.currentStep === 'email-verification') {
this.currentStep = 'register';
} else if (this.currentStep === 'passkey-creation') {
// If email is verified, go back to verification, otherwise to register
this.currentStep = this.emailVerified ? 'email-verification' : 'register';
} else {
this.currentStep = 'login';
}
this.render();
}
// Set loading state
setLoading(loading) {
this.loading = loading;
this.render();
}
// Show error
showError(message) {
this.errorMessage = message;
this.currentStep = 'error';
this.render();
this.dispatchEvent(new CustomEvent('fas-error', {
detail: { message }
}));
}
// Get user-friendly error message
getErrorMessage(error) {
const message = error.message || error.toString();
if (message.includes('User not found')) {
return 'Không tìm thấy tài khoản. Vui lòng đăng ký trước.';
}
if (message.includes('already exists')) {
return 'Email đã được đăng ký. Vui lòng đăng nhập.';
}
if (message.includes('cancelled')) {
return 'Bạn đã hủy quá trình xác thực.';
}
if (message.includes('timeout')) {
return 'Quá thời gian chờ. Vui lòng thử lại.';
}
if (message.includes('not supported')) {
return 'Trình duyệt không hỗ trợ Passkey.';
}
return `Lỗi: ${message}`;
}
// Public API methods
reset() {
this.currentStep = 'login';
this.authData = null;
this.errorMessage = null;
this.userEmail = '';
this.emailVerified = false;
this.render();
}
getAuthData() {
return this.authData;
}
// Handle skip passkey creation
handleSkip() {
// Continue with traditional authentication
this.currentStep = 'success';
this.authData = { user: { email: this.userEmail } };
this.dispatchEvent(new CustomEvent('fas-success', {
detail: { type: 'skip-passkey', user: { email: this.userEmail } }
}));
this.render();
}
// Switch to register
switchToRegister() {
this.currentStep = 'register';
this.render();
}
// Switch to login
switchToLogin() {
this.currentStep = 'login';
this.render();
}
}
// Register the custom element
customElements.define('fas-element', FaSElement);
export { FaSElement as default };
//# sourceMappingURL=fas-element.esm.js.map