central-auth-js-sdk 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Login screen factory — renders screen for current login step
3
+ */
4
+
5
+ import { LOGIN_STEPS } from '../../../core/login-flow.js';
6
+ import { LOGIN_METHODS } from '../../../core/login-config.js';
7
+ import { getEnabledLoginMethods } from '../../../core/login-config.js';
8
+ import { createLoginEntryScreen } from './entry.js';
9
+ import { createLoginIdentifierEmailScreen } from './identifier-email.js';
10
+ import { createLoginIdentifierMobileScreen } from './identifier-mobile.js';
11
+ import { createLoginPasswordScreen } from './password.js';
12
+ import { createLoginOtpScreen } from './otp.js';
13
+ import { createLoginFrsScreen } from './frs.js';
14
+ import { createLoginSuccessScreen } from './success.js';
15
+
16
+ export function createLoginScreen(stepId, flow, state, config, handlers) {
17
+ const {
18
+ onIdentifierEmail,
19
+ onIdentifierMobile,
20
+ onPassword,
21
+ onOtp,
22
+ onFrs,
23
+ onSuccessContinue,
24
+ onBack,
25
+ onBackToApp,
26
+ onSelectMethod,
27
+ onSocialRedirect,
28
+ onSignUp,
29
+ onSwitchToOtp,
30
+ } = handlers;
31
+
32
+ const enabledMethods = getEnabledLoginMethods(config);
33
+ const isFirstScreenSingleMethod = enabledMethods.length === 1;
34
+
35
+ switch (stepId) {
36
+ case LOGIN_STEPS.METHOD_SELECT: {
37
+ const methods = enabledMethods;
38
+ return createLoginEntryScreen(
39
+ methods,
40
+ (methodId) => {
41
+ if (methodId === LOGIN_METHODS.SOCIAL_GOOGLE || methodId === LOGIN_METHODS.SOCIAL_MICROSOFT) {
42
+ onSocialRedirect(methodId === LOGIN_METHODS.SOCIAL_GOOGLE ? 'google' : 'microsoft');
43
+ return;
44
+ }
45
+ onSelectMethod(methodId);
46
+ },
47
+ onSignUp,
48
+ onBackToApp
49
+ );
50
+ }
51
+
52
+ case LOGIN_STEPS.IDENTIFIER_EMAIL:
53
+ return createLoginIdentifierEmailScreen(
54
+ (email) => { state.set('identifier', email); onIdentifierEmail(email); },
55
+ isFirstScreenSingleMethod ? onBackToApp : onBack,
56
+ onSignUp
57
+ );
58
+
59
+ case LOGIN_STEPS.IDENTIFIER_MOBILE:
60
+ return createLoginIdentifierMobileScreen(
61
+ (mobile) => { state.set('identifier', mobile); onIdentifierMobile(mobile); },
62
+ isFirstScreenSingleMethod ? onBackToApp : onBack,
63
+ onSignUp
64
+ );
65
+
66
+ case LOGIN_STEPS.PASSWORD: {
67
+ const identifier = state.get('identifier') || '';
68
+ return createLoginPasswordScreen(
69
+ identifier,
70
+ (password) => { state.set('password', password); onPassword(password); },
71
+ onBack
72
+ );
73
+ }
74
+
75
+ case LOGIN_STEPS.OTP: {
76
+ const identifier = state.get('identifier') || '';
77
+ const method = flow.currentMethod;
78
+ const otpLength = (method === LOGIN_METHODS.EMAIL_OTP) ? 6 : 4;
79
+ return createLoginOtpScreen(
80
+ identifier,
81
+ otpLength,
82
+ (otp) => { state.set('otp', otp); onOtp(otp); },
83
+ onBack
84
+ );
85
+ }
86
+
87
+ case LOGIN_STEPS.FRS:
88
+ return createLoginFrsScreen(
89
+ (dataUrl) => { state.set('faceCapture', dataUrl); onFrs(dataUrl); },
90
+ onBack,
91
+ {
92
+ onSwitchToOtp,
93
+ showOtpFallback: flow.currentMethod === LOGIN_METHODS.EMAIL_FRS,
94
+ }
95
+ );
96
+
97
+ case LOGIN_STEPS.SUCCESS:
98
+ return createLoginSuccessScreen(onSuccessContinue);
99
+
100
+ default:
101
+ return document.createElement('div');
102
+ }
103
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Login — OTP verification (email or mobile)
3
+ * v2.0 — warm, modern design
4
+ */
5
+
6
+ const MAIL_ICON = 'M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z';
7
+ const ARROW_LEFT = 'M19 12H5m7 7l-7-7 7-7';
8
+ const ARROW_RIGHT = 'M5 12h14m-7-7l7 7-7 7';
9
+
10
+ export function createLoginOtpScreen(identifier, otpLength = 4, onNext, onBack) {
11
+ const wrap = document.createElement('div');
12
+ wrap.className = 'wf-screen';
13
+
14
+ const otpInputs = Array(otpLength).fill(0).map((_, i) => `
15
+ <input type="text" inputmode="numeric" maxlength="1" class="wf-otp-input" id="wf-login-otp-${i}" data-idx="${i}" autocomplete="one-time-code" />
16
+ `).join('');
17
+
18
+ wrap.innerHTML = `
19
+ <div class="wf-screen-header">
20
+ <div class="wf-icon-box">
21
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${MAIL_ICON}"/></svg>
22
+ </div>
23
+ <h2 class="wf-screen-title">Check your code</h2>
24
+ <p class="wf-screen-desc">We sent a ${otpLength}-digit code to <span class="wf-highlight">${identifier}</span></p>
25
+ </div>
26
+ <form class="wf-form" id="wf-login-otp-form">
27
+ <div class="wf-field">
28
+ <label class="wf-label">Verification code</label>
29
+ <div class="wf-otp-row">${otpInputs}</div>
30
+ </div>
31
+ <div class="wf-btn-row">
32
+ <button type="button" class="wf-btn wf-btn-outline wf-btn-lg" data-action="back" aria-label="Go back">
33
+ <svg class="wf-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${ARROW_LEFT}"/></svg>
34
+ </button>
35
+ <button type="submit" class="wf-btn wf-btn-primary wf-btn-lg wf-btn-flex">
36
+ Verify
37
+ <svg class="wf-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${ARROW_RIGHT}"/></svg>
38
+ </button>
39
+ </div>
40
+ </form>
41
+ <p class="wf-footer-link">
42
+ Didn't receive the code? <button type="button" class="wf-link">Resend</button>
43
+ </p>
44
+ `;
45
+
46
+ const form = wrap.querySelector('#wf-login-otp-form');
47
+ const otpEls = wrap.querySelectorAll('.wf-otp-input');
48
+
49
+ function handleOtpPaste(e) {
50
+ e.preventDefault();
51
+ const pasted = (e.clipboardData || window.clipboardData).getData('text');
52
+ const digits = pasted.replace(/\D/g, '').slice(0, otpLength).split('');
53
+ digits.forEach((d, i) => { if (otpEls[i]) otpEls[i].value = d; });
54
+ const nextIdx = Math.min(digits.length, otpLength - 1);
55
+ if (otpEls[nextIdx]) otpEls[nextIdx].focus();
56
+ }
57
+
58
+ otpEls.forEach((el, i) => {
59
+ el.addEventListener('paste', handleOtpPaste);
60
+ el.addEventListener('input', (e) => {
61
+ const v = e.target.value.replace(/\D/g, '').slice(0, 1);
62
+ e.target.value = v;
63
+ if (v && i < otpLength - 1) otpEls[i + 1].focus();
64
+ });
65
+ el.addEventListener('keydown', (e) => {
66
+ if (e.key === 'Backspace' && !e.target.value && i > 0) otpEls[i - 1].focus();
67
+ });
68
+ });
69
+
70
+ form.addEventListener('submit', (e) => {
71
+ e.preventDefault();
72
+ const code = [...otpEls].map((o) => o.value).join('');
73
+ if (code.length === otpLength) onNext(code);
74
+ });
75
+
76
+ wrap.querySelector('[data-action="back"]').addEventListener('click', onBack);
77
+
78
+ return wrap;
79
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Login — Password input (for email+password, mobile+password)
3
+ * v2.0 — warm, modern design
4
+ */
5
+
6
+ const LOCK_ICON = 'M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z';
7
+ const ARROW_LEFT = 'M19 12H5m7 7l-7-7 7-7';
8
+ const ARROW_RIGHT = 'M5 12h14m-7-7l7 7-7 7';
9
+ const EYE_ICON = 'M15 12a3 3 0 11-6 0 3 3 0 016 0z M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z';
10
+ const EYE_OFF_ICON = 'M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878a4.5 4.5 0 106.262 6.262M4.031 11.117A10.017 10.017 0 002.458 12c1.274 4.057 5.065 7 9.542 7 .532 0 1.054-.052 1.562-.151';
11
+
12
+ export function createLoginPasswordScreen(identifierLabel, onNext, onBack) {
13
+ const wrap = document.createElement('div');
14
+ wrap.className = 'wf-screen';
15
+
16
+ wrap.innerHTML = `
17
+ <div class="wf-screen-header">
18
+ <div class="wf-icon-box">
19
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${LOCK_ICON}"/></svg>
20
+ </div>
21
+ <h2 class="wf-screen-title">Enter your password</h2>
22
+ <p class="wf-screen-desc">Signing in as <span class="wf-highlight">${identifierLabel}</span></p>
23
+ </div>
24
+ <form class="wf-form" id="wf-login-password-form">
25
+ <div class="wf-field">
26
+ <label class="wf-label" for="wf-login-password">Password</label>
27
+ <div class="wf-input-wrap wf-input-password">
28
+ <svg class="wf-input-icon-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${LOCK_ICON}"/></svg>
29
+ <input type="password" id="wf-login-password" placeholder="Your password" class="wf-input" required autocomplete="current-password" />
30
+ <button type="button" class="wf-input-toggle" data-target="wf-login-password" aria-label="Toggle password">
31
+ <svg class="wf-eye" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${EYE_ICON}"/></svg>
32
+ <svg class="wf-eye-off hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${EYE_OFF_ICON}"/></svg>
33
+ </button>
34
+ </div>
35
+ </div>
36
+ <div class="wf-btn-row">
37
+ <button type="button" class="wf-btn wf-btn-outline wf-btn-lg" data-action="back" aria-label="Go back">
38
+ <svg class="wf-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${ARROW_LEFT}"/></svg>
39
+ </button>
40
+ <button type="submit" class="wf-btn wf-btn-primary wf-btn-lg wf-btn-flex">
41
+ Sign in
42
+ <svg class="wf-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${ARROW_RIGHT}"/></svg>
43
+ </button>
44
+ </div>
45
+ </form>
46
+ `;
47
+
48
+ wrap.querySelector('#wf-login-password-form').addEventListener('submit', (e) => {
49
+ e.preventDefault();
50
+ const p = wrap.querySelector('#wf-login-password').value;
51
+ if (p) onNext(p);
52
+ });
53
+
54
+ wrap.querySelector('.wf-input-toggle').addEventListener('click', () => {
55
+ const input = wrap.querySelector('#wf-login-password');
56
+ const isPass = input.type === 'password';
57
+ input.type = isPass ? 'text' : 'password';
58
+ wrap.querySelector('.wf-eye').classList.toggle('hidden', isPass);
59
+ wrap.querySelector('.wf-eye-off').classList.toggle('hidden', !isPass);
60
+ });
61
+
62
+ wrap.querySelector('[data-action="back"]').addEventListener('click', onBack);
63
+
64
+ return wrap;
65
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Login success screen
3
+ * v2.0 — warm, modern design
4
+ */
5
+
6
+ const CHECK_CIRCLE = 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z';
7
+ const ARROW_RIGHT = 'M5 12h14m-7-7l7 7-7 7';
8
+
9
+ export function createLoginSuccessScreen(onContinue) {
10
+ const wrap = document.createElement('div');
11
+ wrap.className = 'wf-screen wf-screen-success';
12
+
13
+ wrap.innerHTML = `
14
+ <div class="wf-success-icon">
15
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${CHECK_CIRCLE}"/></svg>
16
+ </div>
17
+ <div class="wf-screen-header">
18
+ <h2 class="wf-screen-title">You're signed in</h2>
19
+ <p class="wf-screen-desc">Welcome back. You can now continue to the application.</p>
20
+ </div>
21
+ <div class="wf-success-actions">
22
+ <button type="button" class="wf-btn wf-btn-success wf-btn-lg wf-btn-full" id="wf-login-success-continue">
23
+ Continue to application
24
+ <svg class="wf-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${ARROW_RIGHT}"/></svg>
25
+ </button>
26
+ </div>
27
+ `;
28
+
29
+ wrap.querySelector('#wf-login-success-continue').addEventListener('click', () => onContinue && onContinue());
30
+
31
+ return wrap;
32
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Mobile Entry Screen — Add mobile number (country code + number)
3
+ * v2.0 — warm, modern design
4
+ */
5
+
6
+ const PHONE_ICON = 'M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07 19.5 19.5 0 01-6-6 19.79 19.79 0 01-3.07-8.67A2 2 0 014.11 2h3a2 2 0 012 1.72 12.84 12.84 0 00.7 2.81 2 2 0 0102.78 12.84 12.84 0 006 6 2 2 0 002.78 0 12.84 12.84 0 002.81-.7A2 2 0 0022 16.92z';
7
+ const ARROW_LEFT = 'M19 12H5m7 7l-7-7 7-7';
8
+ const ARROW_RIGHT = 'M5 12h14m-7-7l7 7-7 7';
9
+
10
+ const COUNTRY_CODES = [
11
+ { code: '91', label: '+91' },
12
+ { code: '1', label: '+1' },
13
+ { code: '44', label: '+44' },
14
+ { code: '61', label: '+61' },
15
+ { code: '81', label: '+81' },
16
+ { code: '49', label: '+49' },
17
+ { code: '33', label: '+33' },
18
+ { code: '86', label: '+86' },
19
+ { code: '971', label: '+971' },
20
+ { code: '65', label: '+65' },
21
+ { code: '55', label: '+55' },
22
+ { code: '52', label: '+52' },
23
+ ];
24
+
25
+ export function createMobileEntryScreen(onNext, onBack) {
26
+ const wrap = document.createElement('div');
27
+ wrap.className = 'wf-screen';
28
+
29
+ const countryOptions = COUNTRY_CODES.map((c) =>
30
+ `<option value="${c.code}"${c.code === '91' ? ' selected' : ''}>${c.label}</option>`
31
+ ).join('');
32
+
33
+ wrap.innerHTML = `
34
+ <div class="wf-screen-header">
35
+ <div class="wf-icon-box">
36
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${PHONE_ICON}"/></svg>
37
+ </div>
38
+ <h2 class="wf-screen-title">Add your mobile number</h2>
39
+ <p class="wf-screen-desc">We'll send a verification code to confirm this number</p>
40
+ </div>
41
+ <form class="wf-form" id="wf-mobile-form">
42
+ <div class="wf-field">
43
+ <label class="wf-label" for="wf-mobile">Mobile number</label>
44
+ <div class="wf-mobile-row">
45
+ <select id="wf-mobile-country" class="wf-input wf-country-select" aria-label="Country code">
46
+ ${countryOptions}
47
+ </select>
48
+ <input type="tel" id="wf-mobile" placeholder="98765 43210" class="wf-input wf-mobile-input" inputmode="numeric" required autocomplete="tel" />
49
+ </div>
50
+ </div>
51
+ <div class="wf-btn-row">
52
+ <button type="button" class="wf-btn wf-btn-outline wf-btn-lg" data-action="back" aria-label="Go back">
53
+ <svg class="wf-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${ARROW_LEFT}"/></svg>
54
+ </button>
55
+ <button type="submit" class="wf-btn wf-btn-primary wf-btn-lg wf-btn-flex">
56
+ Send Code
57
+ <svg class="wf-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${ARROW_RIGHT}"/></svg>
58
+ </button>
59
+ </div>
60
+ </form>
61
+ `;
62
+
63
+ const form = wrap.querySelector('#wf-mobile-form');
64
+ const countrySelect = wrap.querySelector('#wf-mobile-country');
65
+ const input = wrap.querySelector('#wf-mobile');
66
+
67
+ form.addEventListener('submit', (e) => {
68
+ e.preventDefault();
69
+ const digits = input.value.trim().replace(/\D/g, '');
70
+ if (digits.length >= 10) {
71
+ const full = '+' + countrySelect.value + digits;
72
+ onNext(full);
73
+ }
74
+ });
75
+
76
+ wrap.querySelector('[data-action="back"]').addEventListener('click', onBack);
77
+
78
+ return wrap;
79
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Mobile Verification Screen — OTP entry
3
+ * v2.0 — warm, modern design
4
+ */
5
+
6
+ const SMARTPHONE_ICON = 'M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z';
7
+ const ARROW_LEFT = 'M19 12H5m7 7l-7-7 7-7';
8
+ const ARROW_RIGHT = 'M5 12h14m-7-7l7 7-7 7';
9
+
10
+ export function createMobileVerifyScreen(mobile, onNext, onBack) {
11
+ const wrap = document.createElement('div');
12
+ wrap.className = 'wf-screen';
13
+
14
+ const otpInputs = Array(4).fill(0).map((_, i) => `
15
+ <input type="text" inputmode="numeric" maxlength="1" class="wf-otp-input" id="wf-mobile-otp-${i}" data-idx="${i}" autocomplete="one-time-code" />
16
+ `).join('');
17
+
18
+ wrap.innerHTML = `
19
+ <div class="wf-screen-header">
20
+ <div class="wf-icon-box">
21
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${SMARTPHONE_ICON}"/></svg>
22
+ </div>
23
+ <h2 class="wf-screen-title">Verify your mobile</h2>
24
+ <p class="wf-screen-desc">Enter the code sent to <span class="wf-highlight">${mobile}</span></p>
25
+ </div>
26
+ <form class="wf-form" id="wf-mobile-verify-form">
27
+ <div class="wf-field">
28
+ <label class="wf-label">Verification code</label>
29
+ <div class="wf-otp-row">${otpInputs}</div>
30
+ </div>
31
+ <div class="wf-btn-row">
32
+ <button type="button" class="wf-btn wf-btn-outline wf-btn-lg" data-action="back" aria-label="Go back">
33
+ <svg class="wf-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${ARROW_LEFT}"/></svg>
34
+ </button>
35
+ <button type="submit" class="wf-btn wf-btn-primary wf-btn-lg wf-btn-flex">
36
+ Verify
37
+ <svg class="wf-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${ARROW_RIGHT}"/></svg>
38
+ </button>
39
+ </div>
40
+ </form>
41
+ <p class="wf-footer-link">
42
+ Didn't receive the code? <button type="button" class="wf-link">Resend</button>
43
+ </p>
44
+ `;
45
+
46
+ const form = wrap.querySelector('#wf-mobile-verify-form');
47
+ const otpEls = wrap.querySelectorAll('.wf-otp-input');
48
+
49
+ function handleOtpPaste(e) {
50
+ e.preventDefault();
51
+ const pasted = (e.clipboardData || window.clipboardData).getData('text');
52
+ const digits = pasted.replace(/\D/g, '').slice(0, 4).split('');
53
+ digits.forEach((d, i) => { if (otpEls[i]) otpEls[i].value = d; });
54
+ const nextIdx = Math.min(digits.length, 3);
55
+ if (otpEls[nextIdx]) otpEls[nextIdx].focus();
56
+ }
57
+
58
+ otpEls.forEach((el, i) => {
59
+ el.addEventListener('paste', handleOtpPaste);
60
+ el.addEventListener('input', (e) => {
61
+ const v = e.target.value.replace(/\D/g, '').slice(0, 1);
62
+ e.target.value = v;
63
+ if (v && i < 3) otpEls[i + 1].focus();
64
+ });
65
+ el.addEventListener('keydown', (e) => {
66
+ if (e.key === 'Backspace' && !e.target.value && i > 0) otpEls[i - 1].focus();
67
+ });
68
+ });
69
+
70
+ form.addEventListener('submit', (e) => {
71
+ e.preventDefault();
72
+ const code = [...otpEls].map((o) => o.value).join('');
73
+ if (code.length === 4) onNext(code);
74
+ });
75
+
76
+ wrap.querySelector('[data-action="back"]').addEventListener('click', onBack);
77
+
78
+ return wrap;
79
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Password Setup Screen — Create password with validation rules
3
+ * v2.0 — warm, modern design
4
+ */
5
+
6
+ const LOCK_ICON = 'M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z';
7
+ const ARROW_LEFT = 'M19 12H5m7 7l-7-7 7-7';
8
+ const ARROW_RIGHT = 'M5 12h14m-7-7l7 7-7 7';
9
+ const EYE_ICON = 'M15 12a3 3 0 11-6 0 3 3 0 016 0z M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z';
10
+ const EYE_OFF_ICON = 'M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878a4.5 4.5 0 106.262 6.262M4.031 11.117A10.017 10.017 0 002.458 12c1.274 4.057 5.065 7 9.542 7 .532 0 1.054-.052 1.562-.151m-1.436-2.436A10.05 10.05 0 0112 5c4.478 0 8.268 2.943 9.542 7a10.02 10.02 0 01-.166 1.044';
11
+ const CHECK_ICON = 'M5 13l4 4L19 7';
12
+ const X_ICON = 'M6 18L18 6M6 6l12 12';
13
+
14
+ const RULES = [
15
+ { label: 'At least 8 characters', test: (p) => p.length >= 8 },
16
+ { label: 'One uppercase letter', test: (p) => /[A-Z]/.test(p) },
17
+ { label: 'One number', test: (p) => /\d/.test(p) },
18
+ { label: 'One special character', test: (p) => /[^A-Za-z0-9]/.test(p) },
19
+ ];
20
+
21
+ export function createPasswordSetupScreen(onNext, onBack) {
22
+ const wrap = document.createElement('div');
23
+ wrap.className = 'wf-screen';
24
+
25
+ wrap.innerHTML = `
26
+ <div class="wf-screen-header">
27
+ <div class="wf-icon-box">
28
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${LOCK_ICON}"/></svg>
29
+ </div>
30
+ <h2 class="wf-screen-title">Create your password</h2>
31
+ <p class="wf-screen-desc">Choose a strong password to keep your account secure</p>
32
+ </div>
33
+ <form class="wf-form" id="wf-password-form">
34
+ <div class="wf-field">
35
+ <label class="wf-label" for="wf-password">Password</label>
36
+ <div class="wf-input-wrap wf-input-password">
37
+ <svg class="wf-input-icon-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${LOCK_ICON}"/></svg>
38
+ <input type="password" id="wf-password" placeholder="Create a password" class="wf-input" required autocomplete="new-password" />
39
+ <button type="button" class="wf-input-toggle" data-target="wf-password" aria-label="Toggle password visibility">
40
+ <svg class="wf-eye" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${EYE_ICON}"/></svg>
41
+ <svg class="wf-eye-off hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${EYE_OFF_ICON}"/></svg>
42
+ </button>
43
+ </div>
44
+ </div>
45
+ <div class="wf-password-rules" id="wf-password-rules">
46
+ ${RULES.map((r) => `
47
+ <div class="wf-rule" data-rule="${r.label}">
48
+ <svg class="wf-rule-icon wf-rule-unmet" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${X_ICON}"/></svg>
49
+ <svg class="wf-rule-icon wf-rule-met hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${CHECK_ICON}"/></svg>
50
+ <span>${r.label}</span>
51
+ </div>
52
+ `).join('')}
53
+ </div>
54
+ <div class="wf-field">
55
+ <label class="wf-label" for="wf-confirm">Confirm password</label>
56
+ <div class="wf-input-wrap wf-input-password">
57
+ <svg class="wf-input-icon-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${LOCK_ICON}"/></svg>
58
+ <input type="password" id="wf-confirm" placeholder="Confirm your password" class="wf-input" required autocomplete="new-password" />
59
+ <button type="button" class="wf-input-toggle" data-target="wf-confirm" aria-label="Toggle password visibility">
60
+ <svg class="wf-eye" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${EYE_ICON}"/></svg>
61
+ <svg class="wf-eye-off hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${EYE_OFF_ICON}"/></svg>
62
+ </button>
63
+ </div>
64
+ <p class="wf-error hidden" id="wf-confirm-error">Passwords do not match</p>
65
+ </div>
66
+ <div class="wf-btn-row">
67
+ <button type="button" class="wf-btn wf-btn-outline wf-btn-lg" data-action="back" aria-label="Go back">
68
+ <svg class="wf-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${ARROW_LEFT}"/></svg>
69
+ </button>
70
+ <button type="submit" class="wf-btn wf-btn-primary wf-btn-lg wf-btn-flex" id="wf-password-submit" disabled>
71
+ Continue
72
+ <svg class="wf-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${ARROW_RIGHT}"/></svg>
73
+ </button>
74
+ </div>
75
+ </form>
76
+ `;
77
+
78
+ const form = wrap.querySelector('#wf-password-form');
79
+ const passwordInput = wrap.querySelector('#wf-password');
80
+ const confirmInput = wrap.querySelector('#wf-confirm');
81
+ const submitBtn = wrap.querySelector('#wf-password-submit');
82
+ const confirmError = wrap.querySelector('#wf-confirm-error');
83
+
84
+ function updateRules() {
85
+ const p = passwordInput.value;
86
+ RULES.forEach((r) => {
87
+ const ruleEl = wrap.querySelector(`[data-rule="${r.label}"]`);
88
+ const unmet = ruleEl.querySelector('.wf-rule-unmet');
89
+ const met = ruleEl.querySelector('.wf-rule-met');
90
+ if (r.test(p)) {
91
+ unmet.classList.add('hidden');
92
+ met.classList.remove('hidden');
93
+ } else {
94
+ unmet.classList.remove('hidden');
95
+ met.classList.add('hidden');
96
+ }
97
+ });
98
+ }
99
+
100
+ function updateSubmit() {
101
+ const p = passwordInput.value;
102
+ const c = confirmInput.value;
103
+ const allMet = RULES.every((r) => r.test(p));
104
+ const matching = p === c && c.length > 0;
105
+ submitBtn.disabled = !allMet || !matching;
106
+ confirmError.classList.toggle('hidden', !c || matching);
107
+ }
108
+
109
+ passwordInput.addEventListener('input', () => {
110
+ updateRules();
111
+ updateSubmit();
112
+ });
113
+
114
+ confirmInput.addEventListener('input', updateSubmit);
115
+
116
+ wrap.querySelectorAll('.wf-input-toggle').forEach((btn) => {
117
+ btn.addEventListener('click', () => {
118
+ const target = wrap.querySelector(`#${btn.dataset.target}`);
119
+ const isPass = target.type === 'password';
120
+ target.type = isPass ? 'text' : 'password';
121
+ btn.querySelector('.wf-eye').classList.toggle('hidden', isPass);
122
+ btn.querySelector('.wf-eye-off').classList.toggle('hidden', !isPass);
123
+ });
124
+ });
125
+
126
+ form.addEventListener('submit', (e) => {
127
+ e.preventDefault();
128
+ const p = passwordInput.value;
129
+ const c = confirmInput.value;
130
+ const allMet = RULES.every((r) => r.test(p));
131
+ if (allMet && p === c) onNext(p);
132
+ });
133
+
134
+ wrap.querySelector('[data-action="back"]').addEventListener('click', onBack);
135
+
136
+ return wrap;
137
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * SignUp Success Screen — Completion state
3
+ * v2.0 — warm, modern design
4
+ */
5
+
6
+ const CHECK_CIRCLE = 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z';
7
+ const ARROW_RIGHT = 'M5 12h14m-7-7l7 7-7 7';
8
+
9
+ export function createSuccessScreen(onContinue) {
10
+ const wrap = document.createElement('div');
11
+ wrap.className = 'wf-screen wf-screen-success';
12
+
13
+ wrap.innerHTML = `
14
+ <div class="wf-success-icon">
15
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${CHECK_CIRCLE}"/></svg>
16
+ </div>
17
+ <div class="wf-screen-header">
18
+ <h2 class="wf-screen-title">You're all set!</h2>
19
+ <p class="wf-screen-desc">Your account has been created and verified successfully.<br>You can now sign in and start using the application.</p>
20
+ </div>
21
+ <div class="wf-success-actions">
22
+ <button type="button" class="wf-btn wf-btn-success wf-btn-lg wf-btn-full" data-action="continue">
23
+ Continue to application
24
+ <svg class="wf-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${ARROW_RIGHT}"/></svg>
25
+ </button>
26
+ </div>
27
+ `;
28
+
29
+ wrap.querySelectorAll('[data-action="continue"]').forEach((btn) => {
30
+ btn.addEventListener('click', () => { if (typeof onContinue === 'function') onContinue(); });
31
+ });
32
+
33
+ return wrap;
34
+ }