central-auth-js-sdk 1.2.4 → 1.2.6

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
@@ -53,19 +53,23 @@ export function createApi(baseUrl = DEFAULT_BASE, options = {}) {
53
53
  return request('POST', '/signup/start', { email });
54
54
  },
55
55
 
56
- /** POST /signup/email/verify — Email verify: OTP + names (entity auto-detected from email in backend) */
57
- async signupEmailVerify({ email, otp, first_name, last_name }) {
58
- return request('POST', '/signup/email/verify', {
56
+ /** POST /signup/email/verify — Email verify: OTP + names + org details (entity auto-detected from email in backend) */
57
+ async signupEmailVerify({ email, otp, first_name, last_name, team, sub_team, designation }) {
58
+ const payload = {
59
59
  email,
60
60
  otp,
61
61
  first_name,
62
62
  last_name,
63
- });
63
+ };
64
+ if (team) payload.team = team;
65
+ if (sub_team) payload.sub_team = sub_team;
66
+ if (designation) payload.designation = designation;
67
+ return request('POST', '/signup/email/verify', payload);
64
68
  },
65
69
 
66
70
  /** 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 });
71
+ async signupMobileSendOtp(email, mobile, channel = 'sms') {
72
+ return request('POST', '/signup/mobile/send_otp', { email, mobile, channel });
69
73
  },
70
74
 
71
75
  /** POST /signup/mobile/verify — Mobile verify */
@@ -104,8 +108,8 @@ export function createApi(baseUrl = DEFAULT_BASE, options = {}) {
104
108
  },
105
109
 
106
110
  /** 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 };
111
+ async googleMobileSend(loginToken, mobile, appIdentifier, channel = 'sms') {
112
+ const body = { login_token: loginToken, mobile, channel };
109
113
  if (appIdentifier) body.app_identifier = appIdentifier;
110
114
  return request('POST', '/login/google/mobile/send', body);
111
115
  },
@@ -126,8 +130,8 @@ export function createApi(baseUrl = DEFAULT_BASE, options = {}) {
126
130
  return request('POST', '/login/google/fr/enroll', fd);
127
131
  },
128
132
 
129
- async microsoftMobileSend(loginToken, mobile, appIdentifier) {
130
- const body = { login_token: loginToken, mobile };
133
+ async microsoftMobileSend(loginToken, mobile, appIdentifier, channel = 'sms') {
134
+ const body = { login_token: loginToken, mobile, channel };
131
135
  if (appIdentifier) body.app_identifier = appIdentifier;
132
136
  return request('POST', '/login/microsoft/mobile/send', body);
133
137
  },
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.4",
3
+ "version": "1.2.6",
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
@@ -103,6 +103,9 @@ export function mount(container, options = {}) {
103
103
  email: all.email,
104
104
  mobile: all.mobile,
105
105
  userId: all.userId,
106
+ team: all.team,
107
+ subTeam: all.subTeam,
108
+ designation: all.designation,
106
109
  };
107
110
  if (apiResult && typeof apiResult === 'object' && apiResult !== null) {
108
111
  Object.assign(payload, apiResult);
@@ -141,13 +144,23 @@ export function mount(container, options = {}) {
141
144
 
142
145
  async function handleEmailVerify(data) {
143
146
  state.clearError();
144
- state.setMultiple({ emailOtp: data.otp, firstName: data.firstName, lastName: data.lastName });
147
+ state.setMultiple({
148
+ emailOtp: data.otp,
149
+ firstName: data.firstName,
150
+ lastName: data.lastName,
151
+ team: data.team,
152
+ subTeam: data.subTeam,
153
+ designation: data.designation,
154
+ });
145
155
  try {
146
156
  const res = await api.signupEmailVerify({
147
157
  email: state.get('email'),
148
158
  otp: data.otp,
149
159
  first_name: data.firstName,
150
160
  last_name: data.lastName,
161
+ team: data.team,
162
+ sub_team: data.subTeam,
163
+ designation: data.designation,
151
164
  });
152
165
  const userId = res?.user_id ?? res?.data?.user_id;
153
166
  if (userId) state.set('userId', userId);
@@ -159,18 +172,21 @@ export function mount(container, options = {}) {
159
172
  }
160
173
  }
161
174
 
162
- async function handleMobileSubmit(mobile) {
175
+ async function handleMobileSubmit(mobile, channel = 'sms') {
163
176
  state.clearError();
164
177
  state.set('mobile', mobile);
178
+ state.set('mobileChannel', channel);
165
179
  try {
180
+ let res;
166
181
  if (isSocial()) {
167
182
  const token = state.get('loginToken');
168
183
  const provider = state.get('socialProvider');
169
- if (provider === 'microsoft') await api.microsoftMobileSend(token, mobile, appIdentifier);
170
- else await api.googleMobileSend(token, mobile, appIdentifier);
184
+ if (provider === 'microsoft') res = await api.microsoftMobileSend(token, mobile, appIdentifier, channel);
185
+ else res = await api.googleMobileSend(token, mobile, appIdentifier, channel);
171
186
  } else {
172
- await api.signupMobileSendOtp(state.get('email'), mobile);
187
+ res = await api.signupMobileSendOtp(state.get('email'), mobile, channel);
173
188
  }
189
+ if (res?.channel) state.set('mobileChannel', res.channel);
174
190
  flow.goNext();
175
191
  } catch (err) {
176
192
  const msg = err?.data?.error || err?.message || 'Failed to send code';
@@ -179,6 +195,24 @@ export function mount(container, options = {}) {
179
195
  }
180
196
  }
181
197
 
198
+ async function handleMobileResend(channel = 'sms') {
199
+ state.clearError();
200
+ const mobile = state.get('mobile');
201
+ if (!mobile) throw new Error('Mobile number missing');
202
+ state.set('mobileChannel', channel);
203
+ let res;
204
+ if (isSocial()) {
205
+ const token = state.get('loginToken');
206
+ const provider = state.get('socialProvider');
207
+ if (provider === 'microsoft') res = await api.microsoftMobileSend(token, mobile, appIdentifier, channel);
208
+ else res = await api.googleMobileSend(token, mobile, appIdentifier, channel);
209
+ } else {
210
+ res = await api.signupMobileSendOtp(state.get('email'), mobile, channel);
211
+ }
212
+ if (res?.channel) state.set('mobileChannel', res.channel);
213
+ return res;
214
+ }
215
+
182
216
  async function handleMobileVerify(otp) {
183
217
  state.clearError();
184
218
  state.set('mobileOtp', otp);
@@ -272,6 +306,7 @@ export function mount(container, options = {}) {
272
306
  onEmailVerify: handleEmailVerify,
273
307
  onMobileSubmit: handleMobileSubmit,
274
308
  onMobileVerify: handleMobileVerify,
309
+ onMobileResend: handleMobileResend,
275
310
  onPasswordSubmit: handlePasswordSubmit,
276
311
  onFaceSubmit: handleFaceSubmit,
277
312
  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
+ }
@@ -38,6 +38,20 @@ export function createEmailVerifyScreen(email, onNext, onBack, prefill = {}) {
38
38
  <input type="text" id="wf-last-name" placeholder="Doe" class="wf-input" required autocomplete="family-name" />
39
39
  </div>
40
40
  </div>
41
+ <div class="wf-row-2">
42
+ <div class="wf-field">
43
+ <label class="wf-label" for="wf-team">Team</label>
44
+ <input type="text" id="wf-team" placeholder="Engineering" class="wf-input" autocomplete="organization" />
45
+ </div>
46
+ <div class="wf-field">
47
+ <label class="wf-label" for="wf-sub-team">Sub-team</label>
48
+ <input type="text" id="wf-sub-team" placeholder="Backend" class="wf-input" />
49
+ </div>
50
+ </div>
51
+ <div class="wf-field">
52
+ <label class="wf-label" for="wf-designation">Designation</label>
53
+ <input type="text" id="wf-designation" placeholder="Software Engineer" class="wf-input" autocomplete="organization-title" />
54
+ </div>
41
55
  <div class="wf-btn-row">
42
56
  <button type="button" class="wf-btn wf-btn-outline wf-btn-lg" data-action="back" aria-label="Go back">
43
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_LEFT}"/></svg>
@@ -57,9 +71,15 @@ export function createEmailVerifyScreen(email, onNext, onBack, prefill = {}) {
57
71
  const otpEls = wrap.querySelectorAll('.wf-otp-input');
58
72
  const firstName = wrap.querySelector('#wf-first-name');
59
73
  const lastName = wrap.querySelector('#wf-last-name');
74
+ const team = wrap.querySelector('#wf-team');
75
+ const subTeam = wrap.querySelector('#wf-sub-team');
76
+ const designation = wrap.querySelector('#wf-designation');
60
77
 
61
78
  if (prefill.firstName) firstName.value = prefill.firstName;
62
79
  if (prefill.lastName) lastName.value = prefill.lastName;
80
+ if (prefill.team && team) team.value = prefill.team;
81
+ if (prefill.subTeam && subTeam) subTeam.value = prefill.subTeam;
82
+ if (prefill.designation && designation) designation.value = prefill.designation;
63
83
 
64
84
  function handleOtpPaste(e) {
65
85
  e.preventDefault();
@@ -86,7 +106,14 @@ export function createEmailVerifyScreen(email, onNext, onBack, prefill = {}) {
86
106
  e.preventDefault();
87
107
  const code = [...otpEls].map((o) => o.value).join('');
88
108
  if (code.length === 6 && firstName.value.trim() && lastName.value.trim()) {
89
- onNext({ otp: code, firstName: firstName.value.trim(), lastName: lastName.value.trim() });
109
+ onNext({
110
+ otp: code,
111
+ firstName: firstName.value.trim(),
112
+ lastName: lastName.value.trim(),
113
+ team: team ? team.value.trim() : '',
114
+ subTeam: subTeam ? subTeam.value.trim() : '',
115
+ designation: designation ? designation.value.trim() : '',
116
+ });
90
117
  }
91
118
  });
92
119
 
@@ -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,
@@ -37,7 +38,13 @@ export function createScreen(step, state, flow, callbacks) {
37
38
  state.get('email'),
38
39
  (data) => onEmailVerify(data),
39
40
  onBack,
40
- { firstName: state.get('firstName') || '', lastName: state.get('lastName') || '' }
41
+ {
42
+ firstName: state.get('firstName') || '',
43
+ lastName: state.get('lastName') || '',
44
+ team: state.get('team') || '',
45
+ subTeam: state.get('subTeam') || '',
46
+ designation: state.get('designation') || '',
47
+ }
41
48
  );
42
49
 
43
50
  case STEPS.MOBILE:
@@ -46,8 +53,12 @@ export function createScreen(step, state, flow, callbacks) {
46
53
  case STEPS.MOBILE_VERIFY:
47
54
  return createMobileVerifyScreen(
48
55
  state.get('mobile'),
49
- (otp) => onMobileVerify(otp),
50
- onBack
56
+ (otp, channel) => onMobileVerify(otp, channel),
57
+ onBack,
58
+ {
59
+ channel: state.get('mobileChannel') || 'sms',
60
+ onResend: onMobileResend,
61
+ }
51
62
  );
52
63
 
53
64
  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
  }