central-auth-js-sdk 1.2.5 → 1.2.7

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.
@@ -111,6 +111,9 @@ When **`autoResumeOAuth`** is `true` (default) and **`defaultContainer`** is set
111
111
  | `loginMethods` | object | — | Default login methods (see Login config) |
112
112
  | `frsAsSecondFactor` | boolean | `false` | Default: require face verification after primary auth |
113
113
  | `enableFaceRegistration` | boolean | `true` | Whether sign-up includes face registration step |
114
+ | `collectOrgDetails` | boolean \| object | `true` | Whether to show/collect organization fields (Team, Sub-team, Designation) during registration (`true` \| `false`, or `{ team, subTeam, designation }`). |
115
+ | `mobileVerification` | string | `'india_only'` | Mobile verification mode: `'india_only'` \| `'none'` \| `'all'`. Default `'india_only'` verifies +91 via OTP and auto-skips OTP for international numbers. |
116
+ | `skipMobileOtp` | boolean | `false` | Explicitly skip mobile OTP verification for all numbers when `true`. |
114
117
  | `onContinue` | function | — | Default callback when user clicks Continue on success (see [§ 9](#9-returning-to-the-parent-application)) |
115
118
  | `onBack` | function | — | Default callback when user clicks ← Back on the first screen of signup/login (see [§ 9](#9-returning-to-the-parent-application)) |
116
119
  | `onLoginComplete` | function | — | Default login-complete callback |
@@ -135,6 +138,8 @@ const auth = initializeAuth({
135
138
  appIdentifier: 'my-app',
136
139
  defaultContainer: '#auth-container',
137
140
  autoResumeOAuth: true,
141
+ mobileVerification: 'india_only', // default: verify +91 with OTP, skip for international
142
+ collectOrgDetails: true, // set false to hide Team, Sub-team, Designation
138
143
  loginMethods: {
139
144
  emailPassword: true,
140
145
  emailOtp: true,
@@ -162,6 +167,10 @@ Starts the **sign-up flow** (email + name → verify → mobile → verify → p
162
167
  | `env` | string | no | Environment: `'local'` \| `'dev'` \| `'staging'` \| `'prod'` (default from init or `'local'`) |
163
168
  | `appIdentifier` | string | no | Optional. When provided, sent with relevant API requests; omit to not send. |
164
169
  | `enableFaceRegistration` | boolean | no | Include face registration step (default from init or `true`) |
170
+ | `collectOrgDetails` | boolean \| object | no | Show/collect Team, Sub-team, Designation during registration (default: `true`, set `false` to hide all, or pass `{ team, subTeam, designation }` for granular control). See [§ 5.3](#53-organization-details-team-sub-team--designation). |
171
+ | `showOrgDetails` | boolean | no | Alias for `collectOrgDetails`. |
172
+ | `mobileVerification` | string | no | Mobile OTP verification policy: `'india_only'` (default) \| `'none'` \| `'all'`. See [§ 5.2](#52-mobile-verification--international-numbers-skip-otp). |
173
+ | `skipMobileOtp` | boolean | no | Skip mobile OTP verification directly (`true` / `false`). |
165
174
  | **`onSignupComplete(payload)`** | function | no | Called **once** when signup completes. `payload`: `email`, `mobile`, `userId` + any fields from final API (e.g. `access_token`, `refresh_token`). |
166
175
  | **`onContinue()`** | function | no | Called when user clicks **Continue to application** on the success screen. Use to unmount and redirect. |
167
176
  | **`onBack()`** | function | no | Called when user clicks **← Back** on the **first screen** of signup. Use to unmount the SDK and return to your app. See [§ 9](#9-returning-to-the-parent-application). |
@@ -279,6 +288,122 @@ auth.startLogin({ theme: { primaryColor: '#2563EB' } }); // overrides to blue f
279
288
 
280
289
  ---
281
290
 
291
+ ## 5.2. Mobile verification & international numbers (Skip OTP)
292
+
293
+ By default, the SDK uses **`mobileVerification: 'india_only'`**. This provides frictionless onboarding for international users while preserving carrier-grade SMS/WhatsApp OTP verification for domestic Indian numbers.
294
+
295
+ ### Verification modes
296
+
297
+ | Mode | Behavior | Button Label | WhatsApp OTP Option |
298
+ |------|----------|--------------|---------------------|
299
+ | `'india_only'` *(Default)* | **+91 (India):** Sends OTP via SMS or WhatsApp and requires 6-digit verification.<br>**International (+1, +44, +971, +65, etc.):** Collects phone number, skips OTP verification, and transitions directly to Password Setup. | "Send Code" (+91)<br>"Continue" (International) | Shown for +91, Hidden for International |
300
+ | `'none'` | Skips OTP for **all** phone numbers. Collects mobile number and transitions immediately to Password Setup. | "Continue" | Hidden |
301
+ | `'all'` | Enforces OTP verification for **all** phone numbers regardless of country code. | "Send Code" | Shown |
302
+
303
+ > **Shortcut:** You can also pass `skipMobileOtp: true` in `startSignup` or `initializeAuth` to bypass mobile OTP verification completely.
304
+
305
+ ### How it works under the hood
306
+
307
+ 1. **Dynamic UI:** When an international country code is selected (or when OTP is skipped), the SDK dynamically switches the CTA button from *"Send Code"* to *"Continue"*, hides the *"Send WhatsApp OTP instead"* button, and updates the prompt text to *"Enter your mobile number to continue"*.
308
+ 2. **Flexible length validation:** Indian numbers (`+91`) require $\ge 10$ digits; international numbers accept between 7 and 15 digits (E.164 standard) to support numbers across different countries (e.g. Singapore `+65` with 8 digits, UAE `+971` with 9 digits).
309
+ 3. **Backend integration:**
310
+ - **Standard flow (with OTP):** Calls `POST /signup/mobile/send_otp` $\rightarrow$ user enters OTP $\rightarrow$ calls `POST /signup/mobile/verify`.
311
+ - **Skip OTP flow:** Calls `POST /signup/mobile/save` with `{ email, mobile }` to persist the mobile number, advances user signup status to `MOBILE_VERIFIED`, and transitions directly to `STEPS.PASSWORD`.
312
+
313
+ ### Example configurations
314
+
315
+ #### 1. Default (India OTP only, international numbers skip OTP)
316
+
317
+ No extra configuration needed! This is the out-of-the-box default:
318
+
319
+ ```javascript
320
+ const instance = startSignup({
321
+ container: '#auth-container',
322
+ env: 'dev',
323
+ // mobileVerification: 'india_only' is active by default
324
+ onSignupComplete(payload) {
325
+ console.log('User signed up:', payload);
326
+ },
327
+ });
328
+ ```
329
+
330
+ #### 2. Skip mobile OTP for all countries
331
+
332
+ ```javascript
333
+ const instance = startSignup({
334
+ container: '#auth-container',
335
+ env: 'dev',
336
+ mobileVerification: 'none', // or pass: skipMobileOtp: true
337
+ onSignupComplete(payload) {
338
+ console.log('Signed up without mobile OTP:', payload);
339
+ },
340
+ });
341
+ ```
342
+
343
+ #### 3. Require OTP for all numbers (Global OTP verification)
344
+
345
+ ```javascript
346
+ const instance = startSignup({
347
+ container: '#auth-container',
348
+ env: 'dev',
349
+ mobileVerification: 'all',
350
+ });
351
+ ```
352
+
353
+ ---
354
+
355
+ ## 5.3. Organization details (Team, Sub-team & Designation)
356
+
357
+ During the email verification step, the SDK by default renders fields to collect **Team**, **Sub-team**, and **Designation** alongside First and Last Name.
358
+
359
+ If your application does not need to collect organization details, or if you only want to collect a subset of them, use **`collectOrgDetails`** (or its alias **`showOrgDetails`**).
360
+
361
+ ### Configuration options
362
+
363
+ | Value | Behavior |
364
+ |-------|----------|
365
+ | `true` *(Default)* | Shows all 3 fields (**Team**, **Sub-team**, **Designation**). |
366
+ | `false` | Completely hides **Team**, **Sub-team**, and **Designation** inputs from the registration screen. The user only enters OTP, First Name, and Last Name. |
367
+ | Object `{ team, subTeam, designation }` | Granular control to show only specific fields (e.g. `{ team: true, subTeam: false, designation: true }`). |
368
+
369
+ ### Examples
370
+
371
+ #### 1. Hide all organization fields during signup
372
+
373
+ ```javascript
374
+ const instance = startSignup({
375
+ container: '#auth-container',
376
+ env: 'dev',
377
+ collectOrgDetails: false, // or showOrgDetails: false
378
+ });
379
+ ```
380
+
381
+ #### 2. Granular control (e.g. collect Team and Designation, hide Sub-team)
382
+
383
+ ```javascript
384
+ const instance = startSignup({
385
+ container: '#auth-container',
386
+ env: 'dev',
387
+ collectOrgDetails: {
388
+ team: true,
389
+ subTeam: false,
390
+ designation: true,
391
+ },
392
+ });
393
+ ```
394
+
395
+ #### 3. Set globally via `initializeAuth`
396
+
397
+ ```javascript
398
+ const auth = initializeAuth({
399
+ env: 'dev',
400
+ defaultContainer: '#auth-container',
401
+ collectOrgDetails: false, // will apply to all startSignup calls by default
402
+ });
403
+ ```
404
+
405
+ ---
406
+
282
407
  ## 6. startLogin(config)
283
408
 
284
409
  Starts the **login flow**. If the parent enables **only one** login method, the SDK skips the method picker and goes straight to that flow.
package/auth.js CHANGED
@@ -122,6 +122,9 @@ export function initializeAuth(config = {}) {
122
122
  loginMethods: null,
123
123
  frsAsSecondFactor: false,
124
124
  enableFaceRegistration: true,
125
+ collectOrgDetails: true,
126
+ mobileVerification: 'india_only',
127
+ skipMobileOtp: false,
125
128
  theme: null,
126
129
  ...config,
127
130
  };
@@ -156,6 +159,10 @@ export function initializeAuth(config = {}) {
156
159
  * @param {string} [config.env] - Environment: 'local' | 'dev' | 'staging' | 'prod' (default from init or 'local')
157
160
  * @param {string} [config.appIdentifier] - Override app identifier
158
161
  * @param {boolean} [config.enableFaceRegistration] - Include face registration step (default from init or true)
162
+ * @param {boolean|Object} [config.collectOrgDetails=true] - Whether to show/collect Team, Sub-team, and Designation during registration (or pass object for granular control: { team, subTeam, designation })
163
+ * @param {boolean} [config.showOrgDetails] - Alias for collectOrgDetails
164
+ * @param {string} [config.mobileVerification] - Mobile verification mode: 'india_only' | 'none' | 'all' (default: 'india_only')
165
+ * @param {boolean} [config.skipMobileOtp] - Explicitly skip mobile OTP verification (default: false)
159
166
  * @param {Function} [config.onSignupComplete] - Called when signup completes with (payload). Payload: email, mobile, userId, plus any token/fields from the final API response (e.g. access_token, refresh_token).
160
167
  * @param {Function} [config.onContinue] - Called when user clicks Continue on the success screen (e.g. unmount and redirect).
161
168
  * @param {Function} [config.onSwitchToLogin] - Called when user clicks "Sign in" (e.g. unmount signup and start login, e.g. login + FRS).
@@ -176,6 +183,10 @@ export function startSignup(config = {}) {
176
183
  apiBaseUrl: resolveApiBaseUrl(merged),
177
184
  appIdentifier: merged.appIdentifier,
178
185
  enableFaceRegistration: merged.enableFaceRegistration,
186
+ collectOrgDetails: config.collectOrgDetails ?? config.showOrgDetails ?? merged.collectOrgDetails,
187
+ showOrgDetails: config.showOrgDetails ?? merged.showOrgDetails,
188
+ mobileVerification: config.mobileVerification ?? merged.mobileVerification,
189
+ skipMobileOtp: config.skipMobileOtp ?? merged.skipMobileOtp,
179
190
  theme: merged.theme || undefined,
180
191
  onSignupComplete: config.onSignupComplete ?? merged.onSignupComplete,
181
192
  onContinue: config.onContinue ?? merged.onContinue,
package/core/api.js CHANGED
@@ -53,14 +53,18 @@ 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 */
@@ -73,6 +77,11 @@ export function createApi(baseUrl = DEFAULT_BASE, options = {}) {
73
77
  return request('POST', '/signup/mobile/verify', { email, mobile, otp });
74
78
  },
75
79
 
80
+ /** POST /signup/mobile/save — Save mobile without OTP verification (e.g. international numbers) */
81
+ async signupMobileSave(email, mobile) {
82
+ return request('POST', '/signup/mobile/save', { email, mobile });
83
+ },
84
+
76
85
  /** POST /signup/password/set — Password step */
77
86
  async signupPasswordSet(email, password) {
78
87
  return request('POST', '/signup/password/set', { email, password });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "central-auth-js-sdk",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
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
@@ -70,10 +70,20 @@ export function mount(container, options = {}) {
70
70
  onContinue = null,
71
71
  onSwitchToLogin = null,
72
72
  onBack = null,
73
+ skipMobileOtp = false,
74
+ mobileVerification = 'india_only',
75
+ collectOrgDetails = true,
76
+ showOrgDetails,
73
77
  } = options;
74
78
 
75
79
  const flow = new FlowController();
76
80
  const state = new StateManager();
81
+ state.set('skipMobileOtp', skipMobileOtp);
82
+ state.set('mobileVerification', mobileVerification);
83
+ state.set('collectOrgDetails', collectOrgDetails);
84
+ if (showOrgDetails !== undefined) {
85
+ state.set('showOrgDetails', showOrgDetails);
86
+ }
77
87
  const api = createApi(apiBaseUrl);
78
88
 
79
89
  const layout = createLayout(el, () => {});
@@ -103,6 +113,9 @@ export function mount(container, options = {}) {
103
113
  email: all.email,
104
114
  mobile: all.mobile,
105
115
  userId: all.userId,
116
+ team: all.team,
117
+ subTeam: all.subTeam,
118
+ designation: all.designation,
106
119
  };
107
120
  if (apiResult && typeof apiResult === 'object' && apiResult !== null) {
108
121
  Object.assign(payload, apiResult);
@@ -141,13 +154,23 @@ export function mount(container, options = {}) {
141
154
 
142
155
  async function handleEmailVerify(data) {
143
156
  state.clearError();
144
- state.setMultiple({ emailOtp: data.otp, firstName: data.firstName, lastName: data.lastName });
157
+ state.setMultiple({
158
+ emailOtp: data.otp,
159
+ firstName: data.firstName,
160
+ lastName: data.lastName,
161
+ team: data.team,
162
+ subTeam: data.subTeam,
163
+ designation: data.designation,
164
+ });
145
165
  try {
146
166
  const res = await api.signupEmailVerify({
147
167
  email: state.get('email'),
148
168
  otp: data.otp,
149
169
  first_name: data.firstName,
150
170
  last_name: data.lastName,
171
+ team: data.team,
172
+ sub_team: data.subTeam,
173
+ designation: data.designation,
151
174
  });
152
175
  const userId = res?.user_id ?? res?.data?.user_id;
153
176
  if (userId) state.set('userId', userId);
@@ -159,11 +182,24 @@ export function mount(container, options = {}) {
159
182
  }
160
183
  }
161
184
 
162
- async function handleMobileSubmit(mobile, channel = 'sms') {
185
+ async function handleMobileSubmit(mobile, channel = 'sms', opts = {}) {
163
186
  state.clearError();
164
187
  state.set('mobile', mobile);
165
188
  state.set('mobileChannel', channel);
166
189
  try {
190
+ const shouldSkipOtp = opts?.skipOtp ?? (
191
+ state.get('skipMobileOtp') === true ||
192
+ state.get('mobileVerification') === 'none' ||
193
+ (state.get('mobileVerification') !== 'all' && !mobile.startsWith('+91'))
194
+ );
195
+
196
+ if (shouldSkipOtp) {
197
+ // Skip OTP verification, save mobile directly and advance to password step
198
+ await api.signupMobileSave(state.get('email'), mobile);
199
+ flow.setStep(STEPS.PASSWORD);
200
+ return;
201
+ }
202
+
167
203
  let res;
168
204
  if (isSocial()) {
169
205
  const token = state.get('loginToken');
@@ -176,7 +212,7 @@ export function mount(container, options = {}) {
176
212
  if (res?.channel) state.set('mobileChannel', res.channel);
177
213
  flow.goNext();
178
214
  } catch (err) {
179
- const msg = err?.data?.error || err?.message || 'Failed to send code';
215
+ const msg = err?.data?.error || err?.message || 'Failed to submit mobile number';
180
216
  state.set('error', msg);
181
217
  showError(msg);
182
218
  }
@@ -7,10 +7,54 @@ const MAIL_CHECK = 'M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2
7
7
  const ARROW_LEFT = 'M19 12H5m7 7l-7-7 7-7';
8
8
  const ARROW_RIGHT = 'M5 12h14m-7-7l7 7-7 7';
9
9
 
10
- export function createEmailVerifyScreen(email, onNext, onBack, prefill = {}) {
10
+ export function createEmailVerifyScreen(email, onNext, onBack, prefill = {}, options = {}) {
11
11
  const wrap = document.createElement('div');
12
12
  wrap.className = 'wf-screen';
13
13
 
14
+ const orgOpt = options.collectOrgDetails !== undefined ? options.collectOrgDetails : options.showOrgDetails;
15
+ const showTeam = typeof orgOpt === 'object' && orgOpt !== null ? Boolean(orgOpt.team) : orgOpt !== false;
16
+ const showSubTeam = typeof orgOpt === 'object' && orgOpt !== null ? Boolean(orgOpt.subTeam) : orgOpt !== false;
17
+ const showDesignation = typeof orgOpt === 'object' && orgOpt !== null ? Boolean(orgOpt.designation) : orgOpt !== false;
18
+
19
+ let orgFieldsHtml = '';
20
+ if (showTeam && showSubTeam) {
21
+ orgFieldsHtml += `
22
+ <div class="wf-row-2">
23
+ <div class="wf-field">
24
+ <label class="wf-label" for="wf-team">Team</label>
25
+ <input type="text" id="wf-team" placeholder="Engineering" class="wf-input" autocomplete="organization" />
26
+ </div>
27
+ <div class="wf-field">
28
+ <label class="wf-label" for="wf-sub-team">Sub-team</label>
29
+ <input type="text" id="wf-sub-team" placeholder="Backend" class="wf-input" />
30
+ </div>
31
+ </div>
32
+ `;
33
+ } else if (showTeam) {
34
+ orgFieldsHtml += `
35
+ <div class="wf-field">
36
+ <label class="wf-label" for="wf-team">Team</label>
37
+ <input type="text" id="wf-team" placeholder="Engineering" class="wf-input" autocomplete="organization" />
38
+ </div>
39
+ `;
40
+ } else if (showSubTeam) {
41
+ orgFieldsHtml += `
42
+ <div class="wf-field">
43
+ <label class="wf-label" for="wf-sub-team">Sub-team</label>
44
+ <input type="text" id="wf-sub-team" placeholder="Backend" class="wf-input" />
45
+ </div>
46
+ `;
47
+ }
48
+
49
+ if (showDesignation) {
50
+ orgFieldsHtml += `
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>
55
+ `;
56
+ }
57
+
14
58
  const otpInputs = Array(6).fill(0).map((_, i) => `
15
59
  <input type="text" inputmode="numeric" maxlength="1" class="wf-otp-input" id="wf-email-otp-${i}" data-idx="${i}" autocomplete="one-time-code" />
16
60
  `).join('');
@@ -38,6 +82,7 @@ export function createEmailVerifyScreen(email, onNext, onBack, prefill = {}) {
38
82
  <input type="text" id="wf-last-name" placeholder="Doe" class="wf-input" required autocomplete="family-name" />
39
83
  </div>
40
84
  </div>
85
+ ${orgFieldsHtml}
41
86
  <div class="wf-btn-row">
42
87
  <button type="button" class="wf-btn wf-btn-outline wf-btn-lg" data-action="back" aria-label="Go back">
43
88
  <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 +102,15 @@ export function createEmailVerifyScreen(email, onNext, onBack, prefill = {}) {
57
102
  const otpEls = wrap.querySelectorAll('.wf-otp-input');
58
103
  const firstName = wrap.querySelector('#wf-first-name');
59
104
  const lastName = wrap.querySelector('#wf-last-name');
105
+ const team = wrap.querySelector('#wf-team');
106
+ const subTeam = wrap.querySelector('#wf-sub-team');
107
+ const designation = wrap.querySelector('#wf-designation');
60
108
 
61
109
  if (prefill.firstName) firstName.value = prefill.firstName;
62
110
  if (prefill.lastName) lastName.value = prefill.lastName;
111
+ if (prefill.team && team) team.value = prefill.team;
112
+ if (prefill.subTeam && subTeam) subTeam.value = prefill.subTeam;
113
+ if (prefill.designation && designation) designation.value = prefill.designation;
63
114
 
64
115
  function handleOtpPaste(e) {
65
116
  e.preventDefault();
@@ -86,7 +137,14 @@ export function createEmailVerifyScreen(email, onNext, onBack, prefill = {}) {
86
137
  e.preventDefault();
87
138
  const code = [...otpEls].map((o) => o.value).join('');
88
139
  if (code.length === 6 && firstName.value.trim() && lastName.value.trim()) {
89
- onNext({ otp: code, firstName: firstName.value.trim(), lastName: lastName.value.trim() });
140
+ onNext({
141
+ otp: code,
142
+ firstName: firstName.value.trim(),
143
+ lastName: lastName.value.trim(),
144
+ team: team ? team.value.trim() : '',
145
+ subTeam: subTeam ? subTeam.value.trim() : '',
146
+ designation: designation ? designation.value.trim() : '',
147
+ });
90
148
  }
91
149
  });
92
150
 
@@ -38,11 +38,24 @@ export function createScreen(step, state, flow, callbacks) {
38
38
  state.get('email'),
39
39
  (data) => onEmailVerify(data),
40
40
  onBack,
41
- { 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
+ },
48
+ {
49
+ collectOrgDetails: state.get('collectOrgDetails'),
50
+ showOrgDetails: state.get('showOrgDetails'),
51
+ }
42
52
  );
43
53
 
44
54
  case STEPS.MOBILE:
45
- return createMobileEntryScreen(onMobileSubmit, onBack);
55
+ return createMobileEntryScreen(onMobileSubmit, onBack, {
56
+ skipMobileOtp: state.get('skipMobileOtp'),
57
+ mobileVerification: state.get('mobileVerification'),
58
+ });
46
59
 
47
60
  case STEPS.MOBILE_VERIFY:
48
61
  return createMobileVerifyScreen(
@@ -23,7 +23,7 @@ const COUNTRY_CODES = [
23
23
  { code: "52", label: "+52" },
24
24
  ];
25
25
 
26
- export function createMobileEntryScreen(onNext, onBack) {
26
+ export function createMobileEntryScreen(onNext, onBack, options = {}) {
27
27
  const wrap = document.createElement("div");
28
28
  wrap.className = "wf-screen";
29
29
 
@@ -37,7 +37,7 @@ export function createMobileEntryScreen(onNext, onBack) {
37
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>
38
38
  </div>
39
39
  <h2 class="wf-screen-title">Add your mobile number</h2>
40
- <p class="wf-screen-desc">We will send a verification code to confirm this number</p>
40
+ <p class="wf-screen-desc" id="wf-mobile-desc">We will send a verification code to confirm this number</p>
41
41
  </div>
42
42
  <form class="wf-form" id="wf-mobile-form">
43
43
  <div class="wf-field">
@@ -54,11 +54,11 @@ export function createMobileEntryScreen(onNext, onBack) {
54
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>
55
55
  </button>
56
56
  <button type="submit" class="wf-btn wf-btn-primary wf-btn-lg wf-btn-flex" id="wf-btn-send-sms">
57
- Send Code
57
+ <span id="wf-mobile-btn-text">Send Code</span>
58
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>
59
59
  </button>
60
60
  </div>
61
- <div class="wf-channel-alt">
61
+ <div class="wf-channel-alt" id="wf-mobile-channel-alt">
62
62
  <div class="wf-divider-text"><span>or</span></div>
63
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
64
  <svg class="wf-icon-sm" viewBox="0 0 24 24" fill="currentColor"><path d="${WHATSAPP_ICON}"/></svg>
@@ -71,14 +71,55 @@ export function createMobileEntryScreen(onNext, onBack) {
71
71
  const form = wrap.querySelector("#wf-mobile-form");
72
72
  const countrySelect = wrap.querySelector("#wf-mobile-country");
73
73
  const input = wrap.querySelector("#wf-mobile");
74
+ const descEl = wrap.querySelector("#wf-mobile-desc");
75
+ const btnTextEl = wrap.querySelector("#wf-mobile-btn-text");
76
+ const channelAlt = wrap.querySelector("#wf-mobile-channel-alt");
74
77
  const waBtn = wrap.querySelector("#wf-btn-send-wa");
75
78
 
79
+ function isSkipOtp() {
80
+ if (options.skipMobileOtp === true || options.mobileVerification === "none") {
81
+ return true;
82
+ }
83
+ if (options.mobileVerification === "all") {
84
+ return false;
85
+ }
86
+ // Default: india_only -> skip OTP for non-+91 numbers
87
+ return countrySelect.value !== "91";
88
+ }
89
+
90
+ function updateUiState() {
91
+ const skip = isSkipOtp();
92
+ if (skip) {
93
+ if (descEl) descEl.textContent = "Enter your mobile number to continue";
94
+ if (btnTextEl) btnTextEl.textContent = "Continue";
95
+ if (channelAlt) channelAlt.style.display = "none";
96
+ } else {
97
+ if (descEl) descEl.textContent = "We will send a verification code to confirm this number";
98
+ if (btnTextEl) btnTextEl.textContent = "Send Code";
99
+ if (channelAlt) channelAlt.style.display = "";
100
+ }
101
+ }
102
+
103
+ countrySelect.addEventListener("change", updateUiState);
104
+ updateUiState();
105
+
106
+ input.addEventListener("input", () => {
107
+ input.setCustomValidity("");
108
+ });
109
+
76
110
  function submitMobile(channel = "sms") {
77
111
  const digits = input.value.trim().replace(/\D/g, "");
78
- if (digits.length >= 10) {
112
+ const minLen = countrySelect.value === "91" ? 10 : 7;
113
+ if (digits.length >= minLen && digits.length <= 15) {
79
114
  const full = "+" + countrySelect.value + digits;
80
- onNext(full, channel);
115
+ const skipOtp = isSkipOtp();
116
+ onNext(full, channel, { skipOtp });
81
117
  } else {
118
+ input.setCustomValidity(
119
+ digits.length < minLen
120
+ ? `Please enter a valid phone number (at least ${minLen} digits)`
121
+ : "Phone number cannot exceed 15 digits"
122
+ );
82
123
  input.focus();
83
124
  if (typeof input.reportValidity === "function") {
84
125
  input.reportValidity();