central-auth-js-sdk 1.2.3 → 1.2.5

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.
package/core/api.js CHANGED
@@ -64,8 +64,8 @@ export function createApi(baseUrl = DEFAULT_BASE, options = {}) {
64
64
  },
65
65
 
66
66
  /** POST /signup/mobile/send_otp — Mobile entry: send mobile OTP */
67
- async signupMobileSendOtp(email, mobile) {
68
- return request('POST', '/signup/mobile/send_otp', { email, mobile });
67
+ async signupMobileSendOtp(email, mobile, channel = 'sms') {
68
+ return request('POST', '/signup/mobile/send_otp', { email, mobile, channel });
69
69
  },
70
70
 
71
71
  /** POST /signup/mobile/verify — Mobile verify */
@@ -104,8 +104,8 @@ export function createApi(baseUrl = DEFAULT_BASE, options = {}) {
104
104
  },
105
105
 
106
106
  /** POST /login/google/mobile/send — after OAuth, send mobile OTP. app_identifier only when provided. */
107
- async googleMobileSend(loginToken, mobile, appIdentifier) {
108
- const body = { login_token: loginToken, mobile };
107
+ async googleMobileSend(loginToken, mobile, appIdentifier, channel = 'sms') {
108
+ const body = { login_token: loginToken, mobile, channel };
109
109
  if (appIdentifier) body.app_identifier = appIdentifier;
110
110
  return request('POST', '/login/google/mobile/send', body);
111
111
  },
@@ -126,8 +126,8 @@ export function createApi(baseUrl = DEFAULT_BASE, options = {}) {
126
126
  return request('POST', '/login/google/fr/enroll', fd);
127
127
  },
128
128
 
129
- async microsoftMobileSend(loginToken, mobile, appIdentifier) {
130
- const body = { login_token: loginToken, mobile };
129
+ async microsoftMobileSend(loginToken, mobile, appIdentifier, channel = 'sms') {
130
+ const body = { login_token: loginToken, mobile, channel };
131
131
  if (appIdentifier) body.app_identifier = appIdentifier;
132
132
  return request('POST', '/login/microsoft/mobile/send', body);
133
133
  },
@@ -11,7 +11,7 @@ export const ENV_API_URLS = {
11
11
  local: 'http://localhost:8080',
12
12
  dev: 'https://central-auth-dev.soham.ai/apis',
13
13
  staging: 'https://central-auth-staging.soham.ai/apis',
14
- prod: 'https://central-auth.soham.ai/apis',
14
+ prod: 'https://central-auth-prod.soham.ai/apis',
15
15
  };
16
16
 
17
17
  /**
package/login.js CHANGED
@@ -153,12 +153,14 @@ export function mountLogin(container, options = {}) {
153
153
  flow.goNext();
154
154
  }
155
155
 
156
- async function handleIdentifierMobile(mobile) {
156
+ async function handleIdentifierMobile(mobile, channel = 'sms') {
157
157
  state.clearError();
158
158
  state.set('identifier', mobile);
159
+ state.set('channel', channel);
159
160
  if (flow.currentMethod === LOGIN_METHODS.MOBILE_OTP) {
160
161
  try {
161
- await api.otpSend(mobile, '', 'sms');
162
+ const res = await api.otpSend(mobile, '', channel);
163
+ if (res?.channel) state.set('channel', res.channel);
162
164
  flow.goNext();
163
165
  } catch (err) {
164
166
  const msg = err?.data?.error || err?.message || 'Failed to send code';
@@ -170,6 +172,16 @@ export function mountLogin(container, options = {}) {
170
172
  flow.goNext();
171
173
  }
172
174
 
175
+ async function handleOtpResend(channel = 'sms') {
176
+ state.clearError();
177
+ const identifier = state.get('identifier');
178
+ if (!identifier) throw new Error('Identifier missing');
179
+ state.set('channel', channel);
180
+ const res = await api.otpSend(identifier, '', channel);
181
+ if (res?.channel) state.set('channel', res.channel);
182
+ return res;
183
+ }
184
+
173
185
  function isFrRequiredError(err) {
174
186
  const msg = (err?.data?.error || err?.message || '').toLowerCase();
175
187
  return msg.includes('facial recognition') && msg.includes('required');
@@ -201,11 +213,14 @@ export function mountLogin(container, options = {}) {
201
213
  }
202
214
  }
203
215
 
204
- async function handleOtp(otp) {
216
+ async function handleOtp(otp, channelParam = null) {
205
217
  state.clearError();
206
218
  state.set('otp', otp);
207
219
  const identifier = state.get('identifier');
208
- const channel = identifier.includes('@') ? 'email' : 'sms';
220
+ let channel = channelParam || state.get('channel');
221
+ if (!channel) {
222
+ channel = identifier.includes('@') ? 'email' : 'sms';
223
+ }
209
224
  if (config.frsAsSecondFactor) {
210
225
  flow.goNext();
211
226
  return;
@@ -308,6 +323,7 @@ export function mountLogin(container, options = {}) {
308
323
  onIdentifierMobile: handleIdentifierMobile,
309
324
  onPassword: handlePassword,
310
325
  onOtp: handleOtp,
326
+ onOtpResend: handleOtpResend,
311
327
  onFrs: handleFrs,
312
328
  onSuccessContinue,
313
329
  onBack: () => { state.clearError(); layout.showError(''); flow.goBack(); },
@@ -330,6 +346,7 @@ export function mountLogin(container, options = {}) {
330
346
  onIdentifierMobile: handlers.onIdentifierMobile,
331
347
  onPassword: handlePassword,
332
348
  onOtp: handleOtp,
349
+ onOtpResend: handlers.onOtpResend,
333
350
  onFrs: handleFrs,
334
351
  onSuccessContinue: handlers.onSuccessContinue,
335
352
  onBack: handlers.onBack,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "central-auth-js-sdk",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
4
4
  "description": "Vanilla JS sign-up and login flow SDK — embeddable auth UI with configurable methods",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/signup.js CHANGED
@@ -159,18 +159,21 @@ export function mount(container, options = {}) {
159
159
  }
160
160
  }
161
161
 
162
- async function handleMobileSubmit(mobile) {
162
+ async function handleMobileSubmit(mobile, channel = 'sms') {
163
163
  state.clearError();
164
164
  state.set('mobile', mobile);
165
+ state.set('mobileChannel', channel);
165
166
  try {
167
+ let res;
166
168
  if (isSocial()) {
167
169
  const token = state.get('loginToken');
168
170
  const provider = state.get('socialProvider');
169
- if (provider === 'microsoft') await api.microsoftMobileSend(token, mobile, appIdentifier);
170
- else await api.googleMobileSend(token, mobile, appIdentifier);
171
+ if (provider === 'microsoft') res = await api.microsoftMobileSend(token, mobile, appIdentifier, channel);
172
+ else res = await api.googleMobileSend(token, mobile, appIdentifier, channel);
171
173
  } else {
172
- await api.signupMobileSendOtp(state.get('email'), mobile);
174
+ res = await api.signupMobileSendOtp(state.get('email'), mobile, channel);
173
175
  }
176
+ if (res?.channel) state.set('mobileChannel', res.channel);
174
177
  flow.goNext();
175
178
  } catch (err) {
176
179
  const msg = err?.data?.error || err?.message || 'Failed to send code';
@@ -179,6 +182,24 @@ export function mount(container, options = {}) {
179
182
  }
180
183
  }
181
184
 
185
+ async function handleMobileResend(channel = 'sms') {
186
+ state.clearError();
187
+ const mobile = state.get('mobile');
188
+ if (!mobile) throw new Error('Mobile number missing');
189
+ state.set('mobileChannel', channel);
190
+ let res;
191
+ if (isSocial()) {
192
+ const token = state.get('loginToken');
193
+ const provider = state.get('socialProvider');
194
+ if (provider === 'microsoft') res = await api.microsoftMobileSend(token, mobile, appIdentifier, channel);
195
+ else res = await api.googleMobileSend(token, mobile, appIdentifier, channel);
196
+ } else {
197
+ res = await api.signupMobileSendOtp(state.get('email'), mobile, channel);
198
+ }
199
+ if (res?.channel) state.set('mobileChannel', res.channel);
200
+ return res;
201
+ }
202
+
182
203
  async function handleMobileVerify(otp) {
183
204
  state.clearError();
184
205
  state.set('mobileOtp', otp);
@@ -272,6 +293,7 @@ export function mount(container, options = {}) {
272
293
  onEmailVerify: handleEmailVerify,
273
294
  onMobileSubmit: handleMobileSubmit,
274
295
  onMobileVerify: handleMobileVerify,
296
+ onMobileResend: handleMobileResend,
275
297
  onPasswordSubmit: handlePasswordSubmit,
276
298
  onFaceSubmit: handleFaceSubmit,
277
299
  onBack: () => {
package/styles.css CHANGED
@@ -1239,3 +1239,99 @@
1239
1239
  .wf-page-footer { margin-top: 0.75rem; }
1240
1240
  .wf-camera-view { aspect-ratio: 16 / 9; }
1241
1241
  }
1242
+
1243
+
1244
+ /* ============================= WHATSAPP & CHANNEL SWITCHING ============================= */
1245
+ .wf-btn-whatsapp-outline {
1246
+ border: 1px solid #25D366;
1247
+ background: rgba(37, 211, 102, 0.05);
1248
+ color: #128C7E;
1249
+ font-weight: 600;
1250
+ }
1251
+ .wf-btn-whatsapp-outline:hover:not(:disabled) {
1252
+ background: rgba(37, 211, 102, 0.12);
1253
+ border-color: #25D366;
1254
+ color: #075E54;
1255
+ transform: translateY(-1px);
1256
+ }
1257
+ .wf-icon-box-whatsapp {
1258
+ background: rgba(37, 211, 102, 0.12) !important;
1259
+ color: #25D366 !important;
1260
+ }
1261
+ .wf-channel-alt {
1262
+ margin-top: 0.75rem;
1263
+ display: flex;
1264
+ flex-direction: column;
1265
+ gap: 0.625rem;
1266
+ }
1267
+ .wf-divider-text {
1268
+ display: flex;
1269
+ align-items: center;
1270
+ text-align: center;
1271
+ color: var(--wf-muted);
1272
+ font-size: 0.75rem;
1273
+ }
1274
+ .wf-divider-text::before,
1275
+ .wf-divider-text::after {
1276
+ content: "";
1277
+ flex: 1;
1278
+ border-bottom: 1px solid var(--wf-border);
1279
+ }
1280
+ .wf-divider-text span {
1281
+ padding: 0 0.625rem;
1282
+ text-transform: uppercase;
1283
+ font-size: 0.6875rem;
1284
+ letter-spacing: 0.05em;
1285
+ font-weight: 600;
1286
+ }
1287
+ .wf-whatsapp-link {
1288
+ display: inline-flex;
1289
+ align-items: center;
1290
+ gap: 0.375rem;
1291
+ color: #128C7E;
1292
+ font-weight: 600;
1293
+ font-size: 0.8125rem;
1294
+ background: none;
1295
+ border: none;
1296
+ cursor: pointer;
1297
+ padding: 0.25rem 0.5rem;
1298
+ border-radius: var(--wf-radius);
1299
+ transition: all 0.15s var(--wf-ease);
1300
+ }
1301
+ .wf-whatsapp-link:hover {
1302
+ background: rgba(37, 211, 102, 0.12);
1303
+ color: #075E54;
1304
+ }
1305
+ .wf-verify-footer {
1306
+ margin-top: 1rem;
1307
+ display: flex;
1308
+ flex-direction: column;
1309
+ align-items: center;
1310
+ gap: 0.5rem;
1311
+ text-align: center;
1312
+ }
1313
+ .wf-channel-switch-wrap {
1314
+ display: flex;
1315
+ align-items: center;
1316
+ justify-content: center;
1317
+ }
1318
+ .wf-status-message {
1319
+ font-size: 0.8125rem;
1320
+ font-weight: 500;
1321
+ padding: 0.375rem 0.75rem;
1322
+ border-radius: var(--wf-radius-sm);
1323
+ transition: all 0.2s ease;
1324
+ width: 100%;
1325
+ }
1326
+ .wf-status-message.success {
1327
+ background: var(--wf-success-bg);
1328
+ color: var(--wf-success);
1329
+ }
1330
+ .wf-status-message.error {
1331
+ background: var(--wf-destructive-bg);
1332
+ color: var(--wf-destructive);
1333
+ }
1334
+ .wf-status-message.info {
1335
+ background: var(--wf-primary-light);
1336
+ color: var(--wf-primary);
1337
+ }
@@ -18,6 +18,7 @@ export function createScreen(step, state, flow, callbacks) {
18
18
  onEmailVerify,
19
19
  onMobileSubmit,
20
20
  onMobileVerify,
21
+ onMobileResend,
21
22
  onPasswordSubmit,
22
23
  onFaceSubmit,
23
24
  onBack,
@@ -46,8 +47,12 @@ export function createScreen(step, state, flow, callbacks) {
46
47
  case STEPS.MOBILE_VERIFY:
47
48
  return createMobileVerifyScreen(
48
49
  state.get('mobile'),
49
- (otp) => onMobileVerify(otp),
50
- onBack
50
+ (otp, channel) => onMobileVerify(otp, channel),
51
+ onBack,
52
+ {
53
+ channel: state.get('mobileChannel') || 'sms',
54
+ onResend: onMobileResend,
55
+ }
51
56
  );
52
57
 
53
58
  case STEPS.PASSWORD:
@@ -1,34 +1,35 @@
1
1
  /**
2
2
  * Login — Mobile identifier input (country code + number)
3
- * v2.0 — warm, modern design
3
+ * v2.0 — warm, modern design with SMS and WhatsApp support
4
4
  */
5
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 010 2.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';
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
+ const WHATSAPP_ICON = "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.888 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L0 24l6.335-1.662c1.746.953 3.71 1.455 5.711 1.456h.005c6.554 0 11.89-5.336 11.893-11.893a11.821 11.821 0 00-3.48-8.413z";
9
10
 
10
11
  const COUNTRY_CODES = [
11
- { code: '91', label: 'India +91' },
12
- { code: '1', label: 'United States +1' },
13
- { code: '44', label: 'United Kingdom +44' },
14
- { code: '61', label: 'Australia +61' },
15
- { code: '81', label: 'Japan +81' },
16
- { code: '49', label: 'Germany +49' },
17
- { code: '33', label: 'France +33' },
18
- { code: '86', label: 'China +86' },
19
- { code: '971', label: 'UAE +971' },
20
- { code: '65', label: 'Singapore +65' },
21
- { code: '55', label: 'Brazil +55' },
22
- { code: '52', label: 'Mexico +52' },
12
+ { code: "91", label: "India +91" },
13
+ { code: "1", label: "United States +1" },
14
+ { code: "44", label: "United Kingdom +44" },
15
+ { code: "61", label: "Australia +61" },
16
+ { code: "81", label: "Japan +81" },
17
+ { code: "49", label: "Germany +49" },
18
+ { code: "33", label: "France +33" },
19
+ { code: "86", label: "China +86" },
20
+ { code: "971", label: "UAE +971" },
21
+ { code: "65", label: "Singapore +65" },
22
+ { code: "55", label: "Brazil +55" },
23
+ { code: "52", label: "Mexico +52" },
23
24
  ];
24
25
 
25
26
  export function createLoginIdentifierMobileScreen(onNext, onBack, onSignUp) {
26
- const wrap = document.createElement('div');
27
- wrap.className = 'wf-screen';
27
+ const wrap = document.createElement("div");
28
+ wrap.className = "wf-screen";
28
29
 
29
30
  const countryOptions = COUNTRY_CODES.map((c) =>
30
- `<option value="${c.code}"${c.code === '91' ? ' selected' : ''}>${c.label}</option>`
31
- ).join('');
31
+ `<option value="${c.code}"${c.code === "91" ? " selected" : ""}>${c.label}</option>`
32
+ ).join("");
32
33
 
33
34
  wrap.innerHTML = `
34
35
  <div class="wf-screen-header">
@@ -36,7 +37,7 @@ export function createLoginIdentifierMobileScreen(onNext, onBack, onSignUp) {
36
37
  <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
38
  </div>
38
39
  <h2 class="wf-screen-title">Enter your mobile number</h2>
39
- <p class="wf-screen-desc">We'll use this to sign you in</p>
40
+ <p class="wf-screen-desc">We will use this to sign you in</p>
40
41
  </div>
41
42
  <form class="wf-form" id="wf-login-mobile-form">
42
43
  <div class="wf-field">
@@ -52,33 +53,58 @@ export function createLoginIdentifierMobileScreen(onNext, onBack, onSignUp) {
52
53
  <button type="button" class="wf-btn wf-btn-outline wf-btn-lg" data-action="back" aria-label="Go back">
53
54
  <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
55
  </button>
55
- <button type="submit" class="wf-btn wf-btn-primary wf-btn-lg wf-btn-flex">
56
+ <button type="submit" class="wf-btn wf-btn-primary wf-btn-lg wf-btn-flex" id="wf-btn-login-submit">
56
57
  Continue
57
58
  <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
59
  </button>
59
60
  </div>
61
+ <div class="wf-channel-alt">
62
+ <div class="wf-divider-text"><span>or</span></div>
63
+ <button type="button" class="wf-btn wf-btn-outline wf-btn-whatsapp-outline wf-btn-lg wf-btn-full" id="wf-btn-login-wa">
64
+ <svg class="wf-icon-sm" viewBox="0 0 24 24" fill="currentColor"><path d="${WHATSAPP_ICON}"/></svg>
65
+ Send WhatsApp OTP instead
66
+ </button>
67
+ </div>
60
68
  </form>
61
69
  <p class="wf-footer-link">
62
70
  Don't have an account? <button type="button" class="wf-link" data-action="signup">Sign up</button>
63
71
  </p>
64
72
  `;
65
73
 
66
- const form = wrap.querySelector('#wf-login-mobile-form');
67
- const countrySelect = wrap.querySelector('#wf-login-mobile-country');
68
- const input = wrap.querySelector('#wf-login-mobile');
74
+ const form = wrap.querySelector("#wf-login-mobile-form");
75
+ const countrySelect = wrap.querySelector("#wf-login-mobile-country");
76
+ const input = wrap.querySelector("#wf-login-mobile");
77
+ const waBtn = wrap.querySelector("#wf-btn-login-wa");
69
78
 
70
- form.addEventListener('submit', (e) => {
71
- e.preventDefault();
72
- const digits = input.value.trim().replace(/\D/g, '');
79
+ function submitMobile(channel = "sms") {
80
+ const digits = input.value.trim().replace(/\D/g, "");
73
81
  if (digits.length >= 10) {
74
- const full = '+' + countrySelect.value + digits;
75
- onNext(full);
82
+ const full = "+" + countrySelect.value + digits;
83
+ onNext(full, channel);
84
+ } else {
85
+ input.focus();
86
+ if (typeof input.reportValidity === "function") {
87
+ input.reportValidity();
88
+ }
76
89
  }
90
+ }
91
+
92
+ form.addEventListener("submit", (e) => {
93
+ e.preventDefault();
94
+ submitMobile("sms");
77
95
  });
78
- wrap.querySelector('[data-action="back"]').addEventListener('click', onBack);
79
- const signUpBtn = wrap.querySelector('[data-action="signup"]');
80
- if (signUpBtn && typeof onSignUp === 'function') {
81
- signUpBtn.addEventListener('click', () => onSignUp());
96
+
97
+ if (waBtn) {
98
+ waBtn.addEventListener("click", (e) => {
99
+ e.preventDefault();
100
+ submitMobile("whatsapp");
101
+ });
102
+ }
103
+
104
+ wrap.querySelector("button[data-action='back']").addEventListener("click", onBack);
105
+ const signUpBtn = wrap.querySelector("button[data-action='signup']");
106
+ if (signUpBtn && typeof onSignUp === "function") {
107
+ signUpBtn.addEventListener("click", () => onSignUp());
82
108
  }
83
109
 
84
110
  return wrap;
@@ -19,6 +19,7 @@ export function createLoginScreen(stepId, flow, state, config, handlers) {
19
19
  onIdentifierMobile,
20
20
  onPassword,
21
21
  onOtp,
22
+ onOtpResend,
22
23
  onFrs,
23
24
  onSuccessContinue,
24
25
  onBack,
@@ -75,13 +76,17 @@ export function createLoginScreen(stepId, flow, state, config, handlers) {
75
76
 
76
77
  case LOGIN_STEPS.OTP: {
77
78
  const identifier = state.get('identifier') || '';
78
- const method = flow.currentMethod;
79
- const otpLength = (method === LOGIN_METHODS.EMAIL_OTP) ? 6 : 4;
79
+ const currentChannel = state.get('channel') || (identifier.includes('@') ? 'email' : 'sms');
80
+ const otpLength = (currentChannel === 'whatsapp' || identifier.includes('@')) ? 6 : 4;
80
81
  return createLoginOtpScreen(
81
82
  identifier,
82
83
  otpLength,
83
- (otp) => { state.set('otp', otp); onOtp(otp); },
84
- onBack
84
+ (otp, chan) => { state.set('otp', otp); onOtp(otp, chan); },
85
+ onBack,
86
+ {
87
+ channel: currentChannel,
88
+ onResend: onOtpResend,
89
+ }
85
90
  );
86
91
  }
87
92
 
@@ -1,79 +1,212 @@
1
1
  /**
2
2
  * Login — OTP verification (email or mobile)
3
- * v2.0 — warm, modern design
3
+ * v2.0 — warm, modern design with SMS and WhatsApp channel switching
4
4
  */
5
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';
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 PHONE_ICON = "M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z";
8
+ const WHATSAPP_ICON = "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.888 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L0 24l6.335-1.662c1.746.953 3.71 1.455 5.711 1.456h.005c6.554 0 11.89-5.336 11.893-11.893a11.821 11.821 0 00-3.48-8.413z";
9
+ const ARROW_LEFT = "M19 12H5m7 7l-7-7 7-7";
10
+ const ARROW_RIGHT = "M5 12h14m-7-7l7 7-7 7";
9
11
 
10
- export function createLoginOtpScreen(identifier, otpLength = 4, onNext, onBack) {
11
- const wrap = document.createElement('div');
12
- wrap.className = 'wf-screen';
12
+ export function createLoginOtpScreen(identifier, otpLength = 4, onNext, onBack, options = {}) {
13
+ const isEmail = identifier.includes("@");
14
+ let currentChannel = (options.channel || (isEmail ? "email" : "sms")).toLowerCase();
15
+ const onResend = typeof options.onResend === "function" ? options.onResend : null;
13
16
 
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
+ const wrap = document.createElement("div");
18
+ wrap.className = "wf-screen";
17
19
 
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>
20
+ let cooldownTimer = null;
21
+ let cooldownSec = 0;
22
+
23
+ function getExpectedLength() {
24
+ if (isEmail) return 6;
25
+ return currentChannel === "whatsapp" ? 6 : 4;
26
+ }
27
+
28
+ function getDescText(len) {
29
+ if (isEmail) {
30
+ return `We sent a ${len}-digit code to <span class="wf-highlight">${identifier}</span>`;
31
+ }
32
+ if (currentChannel === "whatsapp") {
33
+ return `We sent a ${len}-digit code via WhatsApp to <span class="wf-highlight">${identifier}</span>`;
34
+ }
35
+ return `We sent a ${len}-digit code to <span class="wf-highlight">${identifier}</span>`;
36
+ }
37
+
38
+ function renderScreen() {
39
+ const len = getExpectedLength();
40
+ const isWhatsApp = !isEmail && currentChannel === "whatsapp";
41
+
42
+ wrap.innerHTML = `
43
+ <div class="wf-screen-header">
44
+ <div class="wf-icon-box ${isWhatsApp ? "wf-icon-box-whatsapp" : ""}">
45
+ ${isEmail
46
+ ? `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${MAIL_ICON}"/></svg>`
47
+ : isWhatsApp
48
+ ? `<svg viewBox="0 0 24 24" fill="currentColor"><path d="${WHATSAPP_ICON}"/></svg>`
49
+ : `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${PHONE_ICON}"/></svg>`
50
+ }
51
+ </div>
52
+ <h2 class="wf-screen-title">Check your code</h2>
53
+ <p class="wf-screen-desc" id="wf-login-otp-desc">${getDescText(len)}</p>
30
54
  </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>
55
+ <form class="wf-form" id="wf-login-otp-form">
56
+ <div class="wf-field">
57
+ <label class="wf-label">Verification code</label>
58
+ <div class="wf-otp-row" id="wf-login-otp-container">
59
+ ${Array(len).fill(0).map((_, i) => `
60
+ <input type="text" inputmode="numeric" maxlength="1" class="wf-otp-input" id="wf-login-otp-${i}" data-idx="${i}" autocomplete="one-time-code" />
61
+ `).join("")}
62
+ </div>
63
+ </div>
64
+ <div class="wf-btn-row">
65
+ <button type="button" class="wf-btn wf-btn-outline wf-btn-lg" data-action="back" aria-label="Go back">
66
+ <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>
67
+ </button>
68
+ <button type="submit" class="wf-btn wf-btn-primary wf-btn-lg wf-btn-flex">
69
+ Verify
70
+ <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>
71
+ </button>
72
+ </div>
73
+ </form>
74
+ <div class="wf-verify-footer">
75
+ <p class="wf-footer-link">
76
+ Didn't receive the code?
77
+ <button type="button" class="wf-link" id="wf-btn-login-resend">
78
+ ${cooldownSec > 0 ? `Resend (${cooldownSec}s)` : (isWhatsApp ? "Resend WhatsApp OTP" : "Resend")}
79
+ </button>
80
+ </p>
81
+ ${!isEmail ? `
82
+ <div class="wf-channel-switch-wrap">
83
+ ${isWhatsApp
84
+ ? `<button type="button" class="wf-link wf-channel-switch-btn" id="wf-btn-login-switch">Send via SMS instead</button>`
85
+ : `<button type="button" class="wf-whatsapp-link wf-channel-switch-btn" id="wf-btn-login-switch">
86
+ <svg class="wf-icon-sm" viewBox="0 0 24 24" fill="currentColor"><path d="${WHATSAPP_ICON}"/></svg>
87
+ Send WhatsApp OTP instead
88
+ </button>`
89
+ }
90
+ </div>
91
+ ` : ""}
92
+ <div id="wf-login-resend-status" class="wf-status-message" style="display:none;"></div>
39
93
  </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();
94
+ `;
95
+
96
+ bindEvents();
97
+ }
98
+
99
+ function setStatus(msg, type = "success") {
100
+ const el = wrap.querySelector("#wf-login-resend-status");
101
+ if (!el) return;
102
+ el.textContent = msg;
103
+ el.className = `wf-status-message ${type}`;
104
+ el.style.display = "block";
105
+ setTimeout(() => {
106
+ if (el) el.style.display = "none";
107
+ }, 4500);
56
108
  }
57
109
 
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();
110
+ function startCooldown() {
111
+ cooldownSec = 30;
112
+ const resendBtn = wrap.querySelector("#wf-btn-login-resend");
113
+ if (resendBtn) {
114
+ resendBtn.disabled = true;
115
+ resendBtn.textContent = `Resend (${cooldownSec}s)`;
116
+ }
117
+ if (cooldownTimer) clearInterval(cooldownTimer);
118
+ cooldownTimer = setInterval(() => {
119
+ cooldownSec -= 1;
120
+ const btn = wrap.querySelector("#wf-btn-login-resend");
121
+ if (cooldownSec <= 0) {
122
+ clearInterval(cooldownTimer);
123
+ cooldownTimer = null;
124
+ if (btn) {
125
+ btn.disabled = false;
126
+ btn.textContent = currentChannel === "whatsapp" ? "Resend WhatsApp OTP" : "Resend";
127
+ }
128
+ } else if (btn) {
129
+ btn.textContent = `Resend (${cooldownSec}s)`;
130
+ }
131
+ }, 1000);
132
+ }
133
+
134
+ async function triggerResend(targetChannel) {
135
+ if (!onResend) return;
136
+ try {
137
+ const isSwitch = targetChannel !== currentChannel;
138
+ currentChannel = targetChannel;
139
+ if (isSwitch) {
140
+ renderScreen();
141
+ }
142
+ setStatus("Sending verification code...", "info");
143
+ const res = await onResend(targetChannel);
144
+ const chanLabel = targetChannel === "whatsapp" ? "WhatsApp" : (targetChannel === "email" ? "Email" : "SMS");
145
+ setStatus(res?.message || `Code sent via ${chanLabel}!`, "success");
146
+ startCooldown();
147
+ const firstOtp = wrap.querySelector("#wf-login-otp-0");
148
+ if (firstOtp) firstOtp.focus();
149
+ } catch (err) {
150
+ const msg = err?.data?.error || err?.message || "Failed to resend code";
151
+ setStatus(msg, "error");
152
+ }
153
+ }
154
+
155
+ function bindEvents() {
156
+ const form = wrap.querySelector("#wf-login-otp-form");
157
+ const otpEls = wrap.querySelectorAll(".wf-otp-input");
158
+ const len = getExpectedLength();
159
+
160
+ function handleOtpPaste(e) {
161
+ e.preventDefault();
162
+ const pasted = (e.clipboardData || window.clipboardData).getData("text");
163
+ const digits = pasted.replace(/\D/g, "").slice(0, len).split("");
164
+ digits.forEach((d, i) => { if (otpEls[i]) otpEls[i].value = d; });
165
+ const nextIdx = Math.min(digits.length, len - 1);
166
+ if (otpEls[nextIdx]) otpEls[nextIdx].focus();
167
+ }
168
+
169
+ otpEls.forEach((el, i) => {
170
+ el.addEventListener("paste", handleOtpPaste);
171
+ el.addEventListener("input", (e) => {
172
+ const v = e.target.value.replace(/\D/g, "").slice(0, 1);
173
+ e.target.value = v;
174
+ if (v && i < len - 1) otpEls[i + 1].focus();
175
+ });
176
+ el.addEventListener("keydown", (e) => {
177
+ if (e.key === "Backspace" && !e.target.value && i > 0) otpEls[i - 1].focus();
178
+ });
64
179
  });
65
- el.addEventListener('keydown', (e) => {
66
- if (e.key === 'Backspace' && !e.target.value && i > 0) otpEls[i - 1].focus();
180
+
181
+ form.addEventListener("submit", (e) => {
182
+ e.preventDefault();
183
+ const code = [...otpEls].map((o) => o.value).join("");
184
+ if (code.length === len || (code.length >= 4 && code.length <= 6)) {
185
+ onNext(code, currentChannel);
186
+ }
67
187
  });
68
- });
69
188
 
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
- });
189
+ wrap.querySelector("button[data-action='back']").addEventListener("click", onBack);
75
190
 
76
- wrap.querySelector('[data-action="back"]').addEventListener('click', onBack);
191
+ const resendBtn = wrap.querySelector("#wf-btn-login-resend");
192
+ if (resendBtn) {
193
+ if (cooldownSec > 0) resendBtn.disabled = true;
194
+ resendBtn.addEventListener("click", (e) => {
195
+ e.preventDefault();
196
+ if (cooldownSec <= 0) triggerResend(currentChannel);
197
+ });
198
+ }
199
+
200
+ const switchBtn = wrap.querySelector("#wf-btn-login-switch");
201
+ if (switchBtn) {
202
+ switchBtn.addEventListener("click", (e) => {
203
+ e.preventDefault();
204
+ const nextChannel = currentChannel === "whatsapp" ? "sms" : "whatsapp";
205
+ triggerResend(nextChannel);
206
+ });
207
+ }
208
+ }
77
209
 
210
+ renderScreen();
78
211
  return wrap;
79
212
  }
@@ -1,34 +1,35 @@
1
1
  /**
2
2
  * Mobile Entry Screen — Add mobile number (country code + number)
3
- * v2.0 — warm, modern design
3
+ * v2.0 — warm, modern design with SMS and WhatsApp OTP choices
4
4
  */
5
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';
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
+ const WHATSAPP_ICON = "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.888 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L0 24l6.335-1.662c1.746.953 3.71 1.455 5.711 1.456h.005c6.554 0 11.89-5.336 11.893-11.893a11.821 11.821 0 00-3.48-8.413z";
9
10
 
10
11
  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' },
12
+ { code: "91", label: "+91" },
13
+ { code: "1", label: "+1" },
14
+ { code: "44", label: "+44" },
15
+ { code: "61", label: "+61" },
16
+ { code: "81", label: "+81" },
17
+ { code: "49", label: "+49" },
18
+ { code: "33", label: "+33" },
19
+ { code: "86", label: "+86" },
20
+ { code: "971", label: "+971" },
21
+ { code: "65", label: "+65" },
22
+ { code: "55", label: "+55" },
23
+ { code: "52", label: "+52" },
23
24
  ];
24
25
 
25
26
  export function createMobileEntryScreen(onNext, onBack) {
26
- const wrap = document.createElement('div');
27
- wrap.className = 'wf-screen';
27
+ const wrap = document.createElement("div");
28
+ wrap.className = "wf-screen";
28
29
 
29
30
  const countryOptions = COUNTRY_CODES.map((c) =>
30
- `<option value="${c.code}"${c.code === '91' ? ' selected' : ''}>${c.label}</option>`
31
- ).join('');
31
+ `<option value="${c.code}"${c.code === "91" ? " selected" : ""}>${c.label}</option>`
32
+ ).join("");
32
33
 
33
34
  wrap.innerHTML = `
34
35
  <div class="wf-screen-header">
@@ -36,7 +37,7 @@ export function createMobileEntryScreen(onNext, onBack) {
36
37
  <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
38
  </div>
38
39
  <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
+ <p class="wf-screen-desc">We will send a verification code to confirm this number</p>
40
41
  </div>
41
42
  <form class="wf-form" id="wf-mobile-form">
42
43
  <div class="wf-field">
@@ -52,28 +53,52 @@ export function createMobileEntryScreen(onNext, onBack) {
52
53
  <button type="button" class="wf-btn wf-btn-outline wf-btn-lg" data-action="back" aria-label="Go back">
53
54
  <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
55
  </button>
55
- <button type="submit" class="wf-btn wf-btn-primary wf-btn-lg wf-btn-flex">
56
+ <button type="submit" class="wf-btn wf-btn-primary wf-btn-lg wf-btn-flex" id="wf-btn-send-sms">
56
57
  Send Code
57
58
  <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
59
  </button>
59
60
  </div>
61
+ <div class="wf-channel-alt">
62
+ <div class="wf-divider-text"><span>or</span></div>
63
+ <button type="button" class="wf-btn wf-btn-outline wf-btn-whatsapp-outline wf-btn-lg wf-btn-full" id="wf-btn-send-wa">
64
+ <svg class="wf-icon-sm" viewBox="0 0 24 24" fill="currentColor"><path d="${WHATSAPP_ICON}"/></svg>
65
+ Send WhatsApp OTP instead
66
+ </button>
67
+ </div>
60
68
  </form>
61
69
  `;
62
70
 
63
- const form = wrap.querySelector('#wf-mobile-form');
64
- const countrySelect = wrap.querySelector('#wf-mobile-country');
65
- const input = wrap.querySelector('#wf-mobile');
71
+ const form = wrap.querySelector("#wf-mobile-form");
72
+ const countrySelect = wrap.querySelector("#wf-mobile-country");
73
+ const input = wrap.querySelector("#wf-mobile");
74
+ const waBtn = wrap.querySelector("#wf-btn-send-wa");
66
75
 
67
- form.addEventListener('submit', (e) => {
68
- e.preventDefault();
69
- const digits = input.value.trim().replace(/\D/g, '');
76
+ function submitMobile(channel = "sms") {
77
+ const digits = input.value.trim().replace(/\D/g, "");
70
78
  if (digits.length >= 10) {
71
- const full = '+' + countrySelect.value + digits;
72
- onNext(full);
79
+ const full = "+" + countrySelect.value + digits;
80
+ onNext(full, channel);
81
+ } else {
82
+ input.focus();
83
+ if (typeof input.reportValidity === "function") {
84
+ input.reportValidity();
85
+ }
73
86
  }
87
+ }
88
+
89
+ form.addEventListener("submit", (e) => {
90
+ e.preventDefault();
91
+ submitMobile("sms");
74
92
  });
75
93
 
76
- wrap.querySelector('[data-action="back"]').addEventListener('click', onBack);
94
+ if (waBtn) {
95
+ waBtn.addEventListener("click", (e) => {
96
+ e.preventDefault();
97
+ submitMobile("whatsapp");
98
+ });
99
+ }
100
+
101
+ wrap.querySelector("button[data-action='back']").addEventListener("click", onBack);
77
102
 
78
103
  return wrap;
79
104
  }
@@ -1,79 +1,202 @@
1
1
  /**
2
2
  * Mobile Verification Screen — OTP entry
3
- * v2.0 — warm, modern design
3
+ * v2.0 — warm, modern design with SMS and WhatsApp support
4
4
  */
5
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';
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 WHATSAPP_ICON = "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.888 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L0 24l6.335-1.662c1.746.953 3.71 1.455 5.711 1.456h.005c6.554 0 11.89-5.336 11.893-11.893a11.821 11.821 0 00-3.48-8.413z";
8
+ const ARROW_LEFT = "M19 12H5m7 7l-7-7 7-7";
9
+ const ARROW_RIGHT = "M5 12h14m-7-7l7 7-7 7";
9
10
 
10
- export function createMobileVerifyScreen(mobile, onNext, onBack) {
11
- const wrap = document.createElement('div');
12
- wrap.className = 'wf-screen';
11
+ export function createMobileVerifyScreen(mobile, onNext, onBack, options = {}) {
12
+ let currentChannel = (options.channel || "sms").toLowerCase();
13
+ const onResend = typeof options.onResend === "function" ? options.onResend : null;
13
14
 
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('');
15
+ const wrap = document.createElement("div");
16
+ wrap.className = "wf-screen";
17
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>
18
+ let cooldownTimer = null;
19
+ let cooldownSec = 0;
20
+
21
+ function getExpectedLength() {
22
+ return currentChannel === "whatsapp" ? 6 : 4;
23
+ }
24
+
25
+ function getDescText() {
26
+ if (currentChannel === "whatsapp") {
27
+ return `Enter the 6-digit code sent via WhatsApp to <span class="wf-highlight">${mobile}</span>`;
28
+ }
29
+ return `Enter the code sent to <span class="wf-highlight">${mobile}</span>`;
30
+ }
31
+
32
+ function renderScreen() {
33
+ const otpLength = getExpectedLength();
34
+ const isWhatsApp = currentChannel === "whatsapp";
35
+
36
+ wrap.innerHTML = `
37
+ <div class="wf-screen-header">
38
+ <div class="wf-icon-box ${isWhatsApp ? "wf-icon-box-whatsapp" : ""}">
39
+ ${isWhatsApp
40
+ ? `<svg viewBox="0 0 24 24" fill="currentColor"><path d="${WHATSAPP_ICON}"/></svg>`
41
+ : `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${SMARTPHONE_ICON}"/></svg>`
42
+ }
43
+ </div>
44
+ <h2 class="wf-screen-title">Verify your mobile</h2>
45
+ <p class="wf-screen-desc" id="wf-verify-desc">${getDescText()}</p>
30
46
  </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>
47
+ <form class="wf-form" id="wf-mobile-verify-form">
48
+ <div class="wf-field">
49
+ <label class="wf-label">Verification code</label>
50
+ <div class="wf-otp-row" id="wf-otp-container">
51
+ ${Array(otpLength).fill(0).map((_, i) => `
52
+ <input type="text" inputmode="numeric" maxlength="1" class="wf-otp-input" id="wf-mobile-otp-${i}" data-idx="${i}" autocomplete="one-time-code" />
53
+ `).join("")}
54
+ </div>
55
+ </div>
56
+ <div class="wf-btn-row">
57
+ <button type="button" class="wf-btn wf-btn-outline wf-btn-lg" data-action="back" aria-label="Go back">
58
+ <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>
59
+ </button>
60
+ <button type="submit" class="wf-btn wf-btn-primary wf-btn-lg wf-btn-flex">
61
+ Verify
62
+ <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>
63
+ </button>
64
+ </div>
65
+ </form>
66
+ <div class="wf-verify-footer">
67
+ <p class="wf-footer-link">
68
+ Didn't receive the code?
69
+ <button type="button" class="wf-link" id="wf-btn-resend-current">
70
+ ${cooldownSec > 0 ? `Resend (${cooldownSec}s)` : (isWhatsApp ? "Resend WhatsApp OTP" : "Resend")}
71
+ </button>
72
+ </p>
73
+ <div class="wf-channel-switch-wrap">
74
+ ${isWhatsApp
75
+ ? `<button type="button" class="wf-link wf-channel-switch-btn" id="wf-btn-switch-channel">Send via SMS instead</button>`
76
+ : `<button type="button" class="wf-whatsapp-link wf-channel-switch-btn" id="wf-btn-switch-channel">
77
+ <svg class="wf-icon-sm" viewBox="0 0 24 24" fill="currentColor"><path d="${WHATSAPP_ICON}"/></svg>
78
+ Send WhatsApp OTP instead
79
+ </button>`
80
+ }
81
+ </div>
82
+ <div id="wf-resend-status" class="wf-status-message" style="display:none;"></div>
39
83
  </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();
84
+ `;
85
+
86
+ bindEvents();
87
+ }
88
+
89
+ function setStatus(msg, type = "success") {
90
+ const el = wrap.querySelector("#wf-resend-status");
91
+ if (!el) return;
92
+ el.textContent = msg;
93
+ el.className = `wf-status-message ${type}`;
94
+ el.style.display = "block";
95
+ setTimeout(() => {
96
+ if (el) el.style.display = "none";
97
+ }, 4500);
56
98
  }
57
99
 
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();
100
+ function startCooldown() {
101
+ cooldownSec = 30;
102
+ const resendBtn = wrap.querySelector("#wf-btn-resend-current");
103
+ if (resendBtn) {
104
+ resendBtn.disabled = true;
105
+ resendBtn.textContent = `Resend (${cooldownSec}s)`;
106
+ }
107
+ if (cooldownTimer) clearInterval(cooldownTimer);
108
+ cooldownTimer = setInterval(() => {
109
+ cooldownSec -= 1;
110
+ const btn = wrap.querySelector("#wf-btn-resend-current");
111
+ if (cooldownSec <= 0) {
112
+ clearInterval(cooldownTimer);
113
+ cooldownTimer = null;
114
+ if (btn) {
115
+ btn.disabled = false;
116
+ btn.textContent = currentChannel === "whatsapp" ? "Resend WhatsApp OTP" : "Resend";
117
+ }
118
+ } else if (btn) {
119
+ btn.textContent = `Resend (${cooldownSec}s)`;
120
+ }
121
+ }, 1000);
122
+ }
123
+
124
+ async function triggerResend(targetChannel) {
125
+ if (!onResend) return;
126
+ try {
127
+ const isSwitch = targetChannel !== currentChannel;
128
+ currentChannel = targetChannel;
129
+ if (isSwitch) {
130
+ renderScreen();
131
+ }
132
+ setStatus("Sending verification code...", "info");
133
+ const res = await onResend(targetChannel);
134
+ const chanLabel = targetChannel === "whatsapp" ? "WhatsApp" : "SMS";
135
+ setStatus(res?.message || `Code sent via ${chanLabel}!`, "success");
136
+ startCooldown();
137
+ const firstOtp = wrap.querySelector("#wf-mobile-otp-0");
138
+ if (firstOtp) firstOtp.focus();
139
+ } catch (err) {
140
+ const msg = err?.data?.error || err?.message || "Failed to resend code";
141
+ setStatus(msg, "error");
142
+ }
143
+ }
144
+
145
+ function bindEvents() {
146
+ const form = wrap.querySelector("#wf-mobile-verify-form");
147
+ const otpEls = wrap.querySelectorAll(".wf-otp-input");
148
+ const otpLength = getExpectedLength();
149
+
150
+ function handleOtpPaste(e) {
151
+ e.preventDefault();
152
+ const pasted = (e.clipboardData || window.clipboardData).getData("text");
153
+ const digits = pasted.replace(/\D/g, "").slice(0, otpLength).split("");
154
+ digits.forEach((d, i) => { if (otpEls[i]) otpEls[i].value = d; });
155
+ const nextIdx = Math.min(digits.length, otpLength - 1);
156
+ if (otpEls[nextIdx]) otpEls[nextIdx].focus();
157
+ }
158
+
159
+ otpEls.forEach((el, i) => {
160
+ el.addEventListener("paste", handleOtpPaste);
161
+ el.addEventListener("input", (e) => {
162
+ const v = e.target.value.replace(/\D/g, "").slice(0, 1);
163
+ e.target.value = v;
164
+ if (v && i < otpLength - 1) otpEls[i + 1].focus();
165
+ });
166
+ el.addEventListener("keydown", (e) => {
167
+ if (e.key === "Backspace" && !e.target.value && i > 0) otpEls[i - 1].focus();
168
+ });
64
169
  });
65
- el.addEventListener('keydown', (e) => {
66
- if (e.key === 'Backspace' && !e.target.value && i > 0) otpEls[i - 1].focus();
170
+
171
+ form.addEventListener("submit", (e) => {
172
+ e.preventDefault();
173
+ const code = [...otpEls].map((o) => o.value).join("");
174
+ if (code.length === otpLength || (code.length >= 4 && code.length <= 6)) {
175
+ onNext(code, currentChannel);
176
+ }
67
177
  });
68
- });
69
178
 
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
- });
179
+ wrap.querySelector("button[data-action='back']").addEventListener("click", onBack);
75
180
 
76
- wrap.querySelector('[data-action="back"]').addEventListener('click', onBack);
181
+ const resendBtn = wrap.querySelector("#wf-btn-resend-current");
182
+ if (resendBtn) {
183
+ if (cooldownSec > 0) resendBtn.disabled = true;
184
+ resendBtn.addEventListener("click", (e) => {
185
+ e.preventDefault();
186
+ if (cooldownSec <= 0) triggerResend(currentChannel);
187
+ });
188
+ }
189
+
190
+ const switchBtn = wrap.querySelector("#wf-btn-switch-channel");
191
+ if (switchBtn) {
192
+ switchBtn.addEventListener("click", (e) => {
193
+ e.preventDefault();
194
+ const nextChannel = currentChannel === "whatsapp" ? "sms" : "whatsapp";
195
+ triggerResend(nextChannel);
196
+ });
197
+ }
198
+ }
77
199
 
200
+ renderScreen();
78
201
  return wrap;
79
202
  }