central-auth-js-sdk 1.2.9 → 1.2.11

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.
@@ -354,9 +354,13 @@ const instance = startSignup({
354
354
 
355
355
  ## 5.3. Organization details (Team, Sub-team & Designation)
356
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.
357
+ During registration (both email signup verification and Google/Social profile completion), the SDK renders **dynamic dropdown menus** populated with the **Teams**, **Sub-teams**, and **Designations** configured in the Central Auth Admin Interface for your entity.
358
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`**).
359
+ ### Dynamic Dropdown & Hierarchy Features
360
+ - **Dynamic Hierarchy**: The **Sub-team** dropdown dynamically updates its options according to the selected **Team** (displaying only sub-teams mapped to that parent team).
361
+ - **Auto-Resolution**: By default, the SDK automatically detects the entity domain from the user's email (e.g. `jane@jukshio.com` -> `jukshio.com`) and queries `GET /org/structure?entity_domain=...`.
362
+ - **Explicit Entity Configuration**: You can explicitly supply `entityName` (e.g. `'Juksho'`), `entityId`, or `entityDomain` in `startSignup` or `initializeAuth` to bind the signup wizard to a specific tenant.
363
+ - **Granular Visibility**: 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
364
 
361
365
  ### Configuration options
362
366
 
@@ -392,6 +396,16 @@ const instance = startSignup({
392
396
  });
393
397
  ```
394
398
 
399
+ #### 3. Bind to an entity explicitly (e.g. Juksho)
400
+
401
+ ```javascript
402
+ const instance = startSignup({
403
+ container: '#auth-container',
404
+ env: 'dev',
405
+ entityName: 'Juksho', // Loads teams, sub-teams, and designations for Juksho entity
406
+ });
407
+ ```
408
+
395
409
  #### 3. Set globally via `initializeAuth`
396
410
 
397
411
  ```javascript
package/auth.js CHANGED
@@ -125,6 +125,10 @@ export function initializeAuth(config = {}) {
125
125
  collectOrgDetails: true,
126
126
  mobileVerification: 'india_only',
127
127
  skipMobileOtp: false,
128
+ entityName: null,
129
+ entityId: null,
130
+ entityDomain: null,
131
+ orgStructure: null,
128
132
  theme: null,
129
133
  ...config,
130
134
  };
@@ -187,6 +191,10 @@ export function startSignup(config = {}) {
187
191
  showOrgDetails: config.showOrgDetails ?? merged.showOrgDetails,
188
192
  mobileVerification: config.mobileVerification ?? merged.mobileVerification,
189
193
  skipMobileOtp: config.skipMobileOtp ?? merged.skipMobileOtp,
194
+ entityName: config.entityName ?? merged.entityName,
195
+ entityId: config.entityId ?? merged.entityId,
196
+ entityDomain: config.entityDomain ?? merged.entityDomain,
197
+ orgStructure: config.orgStructure ?? merged.orgStructure,
190
198
  theme: merged.theme || undefined,
191
199
  onSignupComplete: config.onSignupComplete ?? merged.onSignupComplete,
192
200
  onContinue: config.onContinue ?? merged.onContinue,
package/core/api.js CHANGED
@@ -82,6 +82,45 @@ export function createApi(baseUrl = DEFAULT_BASE, options = {}) {
82
82
  return request('POST', '/signup/mobile/save', { email, mobile });
83
83
  },
84
84
 
85
+ /** POST /signup/org/details — Save team, sub-team, designation for user (including social login) */
86
+ async signupSaveOrgDetails({ loginToken, email, userId, team, sub_team, designation }) {
87
+ const payload = {};
88
+ if (loginToken) payload.login_token = loginToken;
89
+ if (email) payload.email = email;
90
+ if (userId) payload.user_id = userId;
91
+ if (team !== undefined) payload.team = team;
92
+ if (sub_team !== undefined) payload.sub_team = sub_team;
93
+ if (designation !== undefined) payload.designation = designation;
94
+ return request('POST', '/signup/org/details', payload);
95
+ },
96
+
97
+ /** GET /org/structure — Fetch organization structure (teams, sub-teams, designations) */
98
+ async getOrgStructure({ entityId, entityName, entityDomain } = {}) {
99
+ const tryFetch = async (queryParam) => {
100
+ return request('GET', `/org/structure?${queryParam}`);
101
+ };
102
+
103
+ if (entityId) {
104
+ try {
105
+ const res = await tryFetch(`entity_id=${encodeURIComponent(entityId)}`);
106
+ if (res && (res.teams || res.designations)) return res;
107
+ } catch (_) {}
108
+ }
109
+ if (entityDomain) {
110
+ try {
111
+ const res = await tryFetch(`entity_domain=${encodeURIComponent(entityDomain)}`);
112
+ if (res && (res.teams || res.designations)) return res;
113
+ } catch (_) {}
114
+ }
115
+ if (entityName) {
116
+ try {
117
+ const res = await tryFetch(`entity_name=${encodeURIComponent(entityName)}`);
118
+ if (res && (res.teams || res.designations)) return res;
119
+ } catch (_) {}
120
+ }
121
+ return { teams: [], designations: [] };
122
+ },
123
+
85
124
  /** POST /signup/password/set — Password step */
86
125
  async signupPasswordSet(email, password) {
87
126
  return request('POST', '/signup/password/set', { email, password });
package/core/flow.js CHANGED
@@ -8,6 +8,7 @@ export const STEPS = {
8
8
  EMAIL_VERIFY: 'emailVerify',
9
9
  MOBILE: 'mobile',
10
10
  MOBILE_VERIFY: 'mobileVerify',
11
+ ORG_DETAILS: 'orgDetails',
11
12
  PASSWORD: 'password',
12
13
  FACE: 'face',
13
14
  SUCCESS: 'success',
@@ -32,6 +33,7 @@ const STEP_ORDER = {
32
33
  STEPS.EMAIL,
33
34
  STEPS.MOBILE,
34
35
  STEPS.MOBILE_VERIFY,
36
+ STEPS.ORG_DETAILS,
35
37
  STEPS.FACE,
36
38
  STEPS.SUCCESS,
37
39
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "central-auth-js-sdk",
3
- "version": "1.2.9",
3
+ "version": "1.2.11",
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
@@ -74,6 +74,10 @@ export function mount(container, options = {}) {
74
74
  mobileVerification = 'india_only',
75
75
  collectOrgDetails = true,
76
76
  showOrgDetails,
77
+ entityName = null,
78
+ entityId = null,
79
+ entityDomain = null,
80
+ orgStructure = null,
77
81
  } = options;
78
82
 
79
83
  const flow = new FlowController();
@@ -84,6 +88,10 @@ export function mount(container, options = {}) {
84
88
  if (showOrgDetails !== undefined) {
85
89
  state.set('showOrgDetails', showOrgDetails);
86
90
  }
91
+ if (entityName) state.set('entityName', entityName);
92
+ if (entityId) state.set('entityId', entityId);
93
+ if (entityDomain) state.set('entityDomain', entityDomain);
94
+ if (orgStructure) state.set('orgStructure', orgStructure);
87
95
  const api = createApi(apiBaseUrl);
88
96
 
89
97
  const layout = createLayout(el, () => {});
@@ -96,7 +104,18 @@ export function mount(container, options = {}) {
96
104
  state.set('loginToken', oauth.loginToken);
97
105
  state.set('socialProvider', oauth.provider);
98
106
  if (oauth.email) state.set('email', oauth.email);
107
+ if (oauth.entityDomain) state.set('entityDomain', oauth.entityDomain);
99
108
  clearOAuthParams();
109
+ const socialDomain = oauth.entityDomain || (oauth.email && oauth.email.includes('@') ? oauth.email.split('@')[1] : null);
110
+ api.getOrgStructure({
111
+ entityId: state.get('entityId'),
112
+ entityName: state.get('entityName'),
113
+ entityDomain: socialDomain,
114
+ }).then((res) => {
115
+ if (res && (res.teams || res.designations)) {
116
+ state.set('orgStructure', res);
117
+ }
118
+ }).catch(() => {});
100
119
  flow.setSocialFlow();
101
120
  if (oauth.nextStep === 'FR_ENROLLMENT') {
102
121
  flow.setStep(STEPS.FACE);
@@ -131,6 +150,16 @@ export function mount(container, options = {}) {
131
150
  if (data.lastName) state.set('lastName', data.lastName);
132
151
  }
133
152
  state.set('email', email);
153
+ const emailDomain = state.get('entityDomain') || (email && email.includes('@') ? email.split('@')[1] : null);
154
+ api.getOrgStructure({
155
+ entityId: state.get('entityId'),
156
+ entityName: state.get('entityName'),
157
+ entityDomain: emailDomain,
158
+ }).then((res) => {
159
+ if (res && (res.teams || res.designations)) {
160
+ state.set('orgStructure', res);
161
+ }
162
+ }).catch(() => {});
134
163
  try {
135
164
  const res = await api.signupStart(email);
136
165
  const apiStep = mapNextStep(res?.next_step);
@@ -194,9 +223,14 @@ export function mount(container, options = {}) {
194
223
  );
195
224
 
196
225
  if (shouldSkipOtp) {
197
- // Skip OTP verification, save mobile directly and advance to password step
226
+ // Skip OTP verification, save mobile directly and advance
198
227
  await api.signupMobileSave(state.get('email'), mobile);
199
- flow.setStep(STEPS.PASSWORD);
228
+ if (isSocial()) {
229
+ const skipOrg = state.get('collectOrgDetails') === false || state.get('showOrgDetails') === false;
230
+ flow.setStep(skipOrg ? (enableFaceRegistration ? STEPS.FACE : STEPS.SUCCESS) : STEPS.ORG_DETAILS);
231
+ } else {
232
+ flow.setStep(STEPS.PASSWORD);
233
+ }
200
234
  return;
201
235
  }
202
236
 
@@ -246,6 +280,10 @@ export function mount(container, options = {}) {
246
280
  const mobile = state.get('mobile');
247
281
  if (provider === 'microsoft') await api.microsoftMobileVerify(token, mobile, otp, appIdentifier);
248
282
  else await api.googleMobileVerify(token, mobile, otp, appIdentifier);
283
+
284
+ const skipOrg = state.get('collectOrgDetails') === false || state.get('showOrgDetails') === false;
285
+ flow.setStep(skipOrg ? (enableFaceRegistration ? STEPS.FACE : STEPS.SUCCESS) : STEPS.ORG_DETAILS);
286
+ return;
249
287
  } else {
250
288
  await api.signupMobileVerify(state.get('email'), state.get('mobile'), otp);
251
289
  }
@@ -257,6 +295,34 @@ export function mount(container, options = {}) {
257
295
  }
258
296
  }
259
297
 
298
+ async function handleOrgDetailsSubmit(data) {
299
+ state.clearError();
300
+ state.setMultiple({
301
+ team: data.team,
302
+ subTeam: data.subTeam,
303
+ designation: data.designation,
304
+ });
305
+ try {
306
+ await api.signupSaveOrgDetails({
307
+ loginToken: state.get('loginToken'),
308
+ email: state.get('email'),
309
+ userId: state.get('userId'),
310
+ team: data.team,
311
+ sub_team: data.subTeam,
312
+ designation: data.designation,
313
+ });
314
+ if (enableFaceRegistration) {
315
+ flow.setStep(STEPS.FACE);
316
+ } else {
317
+ flow.setStep(STEPS.SUCCESS);
318
+ }
319
+ } catch (err) {
320
+ const msg = err?.data?.error || err?.message || 'Failed to save organization details';
321
+ state.set('error', msg);
322
+ showError(msg);
323
+ }
324
+ }
325
+
260
326
  async function handlePasswordSubmit(password) {
261
327
  state.clearError();
262
328
  state.set('password', password);
@@ -330,6 +396,7 @@ export function mount(container, options = {}) {
330
396
  onMobileSubmit: handleMobileSubmit,
331
397
  onMobileVerify: handleMobileVerify,
332
398
  onMobileResend: handleMobileResend,
399
+ onOrgDetailsSubmit: handleOrgDetailsSubmit,
333
400
  onPasswordSubmit: handlePasswordSubmit,
334
401
  onFaceSubmit: handleFaceSubmit,
335
402
  onBack: () => {
package/styles.css CHANGED
@@ -557,6 +557,31 @@
557
557
  box-shadow: 0 0 0 3px var(--wf-primary-glow), inset 0 1px 0 rgba(255, 255, 255, 0.5);
558
558
  }
559
559
 
560
+ select.wf-input,
561
+ .wf-select {
562
+ appearance: none;
563
+ -webkit-appearance: none;
564
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%236b7280' stroke-width='2'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
565
+ background-repeat: no-repeat;
566
+ background-position: right 0.875rem center;
567
+ background-size: 1rem 1rem;
568
+ padding-right: 2.25rem;
569
+ cursor: pointer;
570
+ }
571
+
572
+ select.wf-input option,
573
+ .wf-select option {
574
+ background: var(--wf-card);
575
+ color: var(--wf-fg);
576
+ }
577
+
578
+ select.wf-input:disabled,
579
+ .wf-select:disabled {
580
+ opacity: 0.6;
581
+ cursor: not-allowed;
582
+ background-color: var(--wf-muted-bg);
583
+ }
584
+
560
585
  .wf-input::placeholder {
561
586
  color: var(--wf-muted);
562
587
  }
@@ -1,63 +1,33 @@
1
1
  /**
2
- * Email Verification Screen — OTP + First/Last name
2
+ * Email Verification Screen — OTP + First/Last name + Org Hierarchy Dropdowns
3
3
  * v2.0 — warm, modern design
4
4
  */
5
5
 
6
- const MAIL_CHECK = '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
+ import { renderOrgFieldsHtml, attachOrgFieldsLogic } from "./org-fields.js";
7
+
8
+ const MAIL_CHECK = "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";
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
12
  export function createEmailVerifyScreen(email, onNext, onBack, prefill = {}, options = {}) {
11
- const wrap = document.createElement('div');
12
- wrap.className = 'wf-screen';
13
+ const wrap = document.createElement("div");
14
+ wrap.className = "wf-screen";
13
15
 
14
16
  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;
17
+ const showTeam = typeof orgOpt === "object" && orgOpt !== null ? Boolean(orgOpt.team) : orgOpt !== false;
18
+ const showSubTeam = typeof orgOpt === "object" && orgOpt !== null ? Boolean(orgOpt.subTeam) : orgOpt !== false;
19
+ const showDesignation = typeof orgOpt === "object" && orgOpt !== null ? Boolean(orgOpt.designation) : orgOpt !== false;
18
20
 
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
- }
21
+ const orgFieldsHtml = renderOrgFieldsHtml({
22
+ showTeam,
23
+ showSubTeam,
24
+ showDesignation,
25
+ prefix: "wf-",
26
+ });
57
27
 
58
28
  const otpInputs = Array(6).fill(0).map((_, i) => `
59
29
  <input type="text" inputmode="numeric" maxlength="1" class="wf-otp-input" id="wf-email-otp-${i}" data-idx="${i}" autocomplete="one-time-code" />
60
- `).join('');
30
+ `).join("");
61
31
 
62
32
  wrap.innerHTML = `
63
33
  <div class="wf-screen-header">
@@ -98,57 +68,59 @@ export function createEmailVerifyScreen(email, onNext, onBack, prefill = {}, opt
98
68
  </p>
99
69
  `;
100
70
 
101
- const form = wrap.querySelector('#wf-email-verify-form');
102
- const otpEls = wrap.querySelectorAll('.wf-otp-input');
103
- const firstName = wrap.querySelector('#wf-first-name');
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');
71
+ const form = wrap.querySelector("#wf-email-verify-form");
72
+ const otpEls = wrap.querySelectorAll(".wf-otp-input");
73
+ const firstName = wrap.querySelector("#wf-first-name");
74
+ const lastName = wrap.querySelector("#wf-last-name");
108
75
 
109
76
  if (prefill.firstName) firstName.value = prefill.firstName;
110
77
  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;
78
+
79
+ // Initialize and attach dynamic org dropdowns logic
80
+ const orgLogic = attachOrgFieldsLogic({
81
+ wrap,
82
+ prefix: "wf-",
83
+ prefill,
84
+ options,
85
+ });
114
86
 
115
87
  function handleOtpPaste(e) {
116
88
  e.preventDefault();
117
- const pasted = (e.clipboardData || window.clipboardData).getData('text');
118
- const digits = pasted.replace(/\D/g, '').slice(0, 6).split('');
89
+ const pasted = (e.clipboardData || window.clipboardData).getData("text");
90
+ const digits = pasted.replace(/\D/g, "").slice(0, 6).split("");
119
91
  digits.forEach((d, i) => { if (otpEls[i]) otpEls[i].value = d; });
120
92
  const nextIdx = Math.min(digits.length, 5);
121
93
  if (otpEls[nextIdx]) otpEls[nextIdx].focus();
122
94
  }
123
95
 
124
96
  otpEls.forEach((el, i) => {
125
- el.addEventListener('paste', handleOtpPaste);
126
- el.addEventListener('input', (e) => {
127
- const v = e.target.value.replace(/\D/g, '').slice(0, 1);
97
+ el.addEventListener("paste", handleOtpPaste);
98
+ el.addEventListener("input", (e) => {
99
+ const v = e.target.value.replace(/\D/g, "").slice(0, 1);
128
100
  e.target.value = v;
129
101
  if (v && i < 5) otpEls[i + 1].focus();
130
102
  });
131
- el.addEventListener('keydown', (e) => {
132
- if (e.key === 'Backspace' && !e.target.value && i > 0) otpEls[i - 1].focus();
103
+ el.addEventListener("keydown", (e) => {
104
+ if (e.key === "Backspace" && !e.target.value && i > 0) otpEls[i - 1].focus();
133
105
  });
134
106
  });
135
107
 
136
- form.addEventListener('submit', (e) => {
108
+ form.addEventListener("submit", (e) => {
137
109
  e.preventDefault();
138
- const code = [...otpEls].map((o) => o.value).join('');
110
+ const code = [...otpEls].map((o) => o.value).join("");
139
111
  if (code.length === 6 && firstName.value.trim() && lastName.value.trim()) {
140
112
  onNext({
141
113
  otp: code,
142
114
  firstName: firstName.value.trim(),
143
115
  lastName: lastName.value.trim(),
144
- team: team ? team.value.trim() : '',
145
- subTeam: subTeam ? subTeam.value.trim() : '',
146
- designation: designation ? designation.value.trim() : '',
116
+ team: orgLogic.getTeam(),
117
+ subTeam: orgLogic.getSubTeam(),
118
+ designation: orgLogic.getDesignation(),
147
119
  });
148
120
  }
149
121
  });
150
122
 
151
- wrap.querySelector('[data-action="back"]').addEventListener('click', onBack);
123
+ wrap.querySelector('[data-action="back"]').addEventListener("click", onBack);
152
124
 
153
125
  return wrap;
154
126
  }
@@ -3,14 +3,15 @@
3
3
  * Callbacks are async API handlers (called from index.js)
4
4
  */
5
5
 
6
- import { STEPS } from '../../core/flow.js';
7
- import { createEmailEntryScreen } from './email-entry.js';
8
- import { createEmailVerifyScreen } from './email-verify.js';
9
- import { createMobileEntryScreen } from './mobile-entry.js';
10
- import { createMobileVerifyScreen } from './mobile-verify.js';
11
- import { createPasswordSetupScreen } from './password-setup.js';
12
- import { createFaceRegistrationScreen } from './face-registration.js';
13
- import { createSuccessScreen } from './success.js';
6
+ import { STEPS } from "../../core/flow.js";
7
+ import { createEmailEntryScreen } from "./email-entry.js";
8
+ import { createEmailVerifyScreen } from "./email-verify.js";
9
+ import { createMobileEntryScreen } from "./mobile-entry.js";
10
+ import { createMobileVerifyScreen } from "./mobile-verify.js";
11
+ import { createPasswordSetupScreen } from "./password-setup.js";
12
+ import { createOrgDetailsScreen } from "./org-details.js";
13
+ import { createFaceRegistrationScreen } from "./face-registration.js";
14
+ import { createSuccessScreen } from "./success.js";
14
15
 
15
16
  export function createScreen(step, state, flow, callbacks) {
16
17
  const {
@@ -19,6 +20,7 @@ export function createScreen(step, state, flow, callbacks) {
19
20
  onMobileSubmit,
20
21
  onMobileVerify,
21
22
  onMobileResend,
23
+ onOrgDetailsSubmit,
22
24
  onPasswordSubmit,
23
25
  onFaceSubmit,
24
26
  onBack,
@@ -29,45 +31,79 @@ export function createScreen(step, state, flow, callbacks) {
29
31
  api,
30
32
  } = callbacks;
31
33
 
34
+ const email = state.get("email") || "";
35
+ const derivedDomain = email.includes("@") ? email.split("@")[1] : null;
36
+ const entityDomain = state.get("entityDomain") || derivedDomain;
37
+ const entityName = state.get("entityName") || null;
38
+ const entityId = state.get("entityId") || null;
39
+ const orgStructure = state.get("orgStructure") || null;
40
+
32
41
  switch (step) {
33
42
  case STEPS.EMAIL:
34
43
  return createEmailEntryScreen(onEmailSubmit, onSocialSignUp, onSwitchToLogin, onBackToApp);
35
44
 
36
45
  case STEPS.EMAIL_VERIFY:
37
46
  return createEmailVerifyScreen(
38
- state.get('email'),
47
+ email,
39
48
  (data) => onEmailVerify(data),
40
49
  onBack,
41
50
  {
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') || '',
51
+ firstName: state.get("firstName") || "",
52
+ lastName: state.get("lastName") || "",
53
+ team: state.get("team") || "",
54
+ subTeam: state.get("subTeam") || "",
55
+ designation: state.get("designation") || "",
47
56
  },
48
57
  {
49
- collectOrgDetails: state.get('collectOrgDetails'),
50
- showOrgDetails: state.get('showOrgDetails'),
58
+ collectOrgDetails: state.get("collectOrgDetails"),
59
+ showOrgDetails: state.get("showOrgDetails"),
60
+ api,
61
+ entityId,
62
+ entityName,
63
+ entityDomain,
64
+ orgStructure,
65
+ onOrgLoaded: (data) => state.set("orgStructure", data),
51
66
  }
52
67
  );
53
68
 
54
69
  case STEPS.MOBILE:
55
70
  return createMobileEntryScreen(onMobileSubmit, onBack, {
56
- skipMobileOtp: state.get('skipMobileOtp'),
57
- mobileVerification: state.get('mobileVerification'),
71
+ skipMobileOtp: state.get("skipMobileOtp"),
72
+ mobileVerification: state.get("mobileVerification"),
58
73
  });
59
74
 
60
75
  case STEPS.MOBILE_VERIFY:
61
76
  return createMobileVerifyScreen(
62
- state.get('mobile'),
77
+ state.get("mobile"),
63
78
  (otp, channel) => onMobileVerify(otp, channel),
64
79
  onBack,
65
80
  {
66
- channel: state.get('mobileChannel') || 'sms',
81
+ channel: state.get("mobileChannel") || "sms",
67
82
  onResend: onMobileResend,
68
83
  }
69
84
  );
70
85
 
86
+ case STEPS.ORG_DETAILS:
87
+ return createOrgDetailsScreen(
88
+ (data) => onOrgDetailsSubmit(data),
89
+ onBack,
90
+ {
91
+ team: state.get("team") || "",
92
+ subTeam: state.get("subTeam") || "",
93
+ designation: state.get("designation") || "",
94
+ },
95
+ {
96
+ collectOrgDetails: state.get("collectOrgDetails"),
97
+ showOrgDetails: state.get("showOrgDetails"),
98
+ api,
99
+ entityId,
100
+ entityName,
101
+ entityDomain,
102
+ orgStructure,
103
+ onOrgLoaded: (data) => state.set("orgStructure", data),
104
+ }
105
+ );
106
+
71
107
  case STEPS.PASSWORD:
72
108
  return createPasswordSetupScreen(onPasswordSubmit, onBack);
73
109
 
@@ -79,9 +115,9 @@ export function createScreen(step, state, flow, callbacks) {
79
115
  );
80
116
 
81
117
  case STEPS.SUCCESS:
82
- return createSuccessScreen(typeof onContinue === 'function' ? onContinue : () => {});
118
+ return createSuccessScreen(typeof onContinue === "function" ? onContinue : () => {});
83
119
 
84
120
  default:
85
- return document.createElement('div');
121
+ return document.createElement("div");
86
122
  }
87
123
  }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Organization Details Screen — Team, Sub-team & Designation Dropdowns
3
+ * v2.0 — warm, modern design for onboarding & social signup
4
+ */
5
+
6
+ import { renderOrgFieldsHtml, attachOrgFieldsLogic } from "./org-fields.js";
7
+
8
+ const BRIEFCASE_ICON = "M20 7h-4V4c0-1.1-.9-2-2-2h-4c-1.1 0-2 .9-2 2v3H4c-1.1 0-2 .9-2 2v11c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2zM10 4h4v3h-4V4zm10 16H4V9h16v11z";
9
+ const ARROW_LEFT = "M19 12H5m7 7l-7-7 7-7";
10
+ const ARROW_RIGHT = "M5 12h14m-7-7l7 7-7 7";
11
+
12
+ export function createOrgDetailsScreen(onNext, onBack, prefill = {}, options = {}) {
13
+ const wrap = document.createElement("div");
14
+ wrap.className = "wf-screen";
15
+
16
+ const orgOpt = options.collectOrgDetails !== undefined ? options.collectOrgDetails : options.showOrgDetails;
17
+ const showTeam = typeof orgOpt === "object" && orgOpt !== null ? Boolean(orgOpt.team) : orgOpt !== false;
18
+ const showSubTeam = typeof orgOpt === "object" && orgOpt !== null ? Boolean(orgOpt.subTeam) : orgOpt !== false;
19
+ const showDesignation = typeof orgOpt === "object" && orgOpt !== null ? Boolean(orgOpt.designation) : orgOpt !== false;
20
+
21
+ const fieldsHtml = renderOrgFieldsHtml({
22
+ showTeam,
23
+ showSubTeam,
24
+ showDesignation,
25
+ prefix: "wf-org-",
26
+ });
27
+
28
+ wrap.innerHTML = `
29
+ <div class="wf-screen-header">
30
+ <div class="wf-icon-box">
31
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${BRIEFCASE_ICON}"/></svg>
32
+ </div>
33
+ <h2 class="wf-screen-title">Complete your profile</h2>
34
+ <p class="wf-screen-desc">Select your team and role to personalize your workspace</p>
35
+ </div>
36
+ <form class="wf-form" id="wf-org-form">
37
+ ${fieldsHtml}
38
+ <div class="wf-btn-row">
39
+ <button type="button" class="wf-btn wf-btn-outline wf-btn-lg" data-action="back" aria-label="Go back">
40
+ <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>
41
+ </button>
42
+ <button type="submit" class="wf-btn wf-btn-primary wf-btn-lg wf-btn-flex" id="wf-btn-org-submit">
43
+ Continue
44
+ <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>
45
+ </button>
46
+ </div>
47
+ </form>
48
+ `;
49
+
50
+ const form = wrap.querySelector("#wf-org-form");
51
+
52
+ // Initialize and attach dynamic org dropdowns logic
53
+ const orgLogic = attachOrgFieldsLogic({
54
+ wrap,
55
+ prefix: "wf-org-",
56
+ prefill,
57
+ options,
58
+ });
59
+
60
+ form.addEventListener("submit", (e) => {
61
+ e.preventDefault();
62
+ onNext({
63
+ team: orgLogic.getTeam(),
64
+ subTeam: orgLogic.getSubTeam(),
65
+ designation: orgLogic.getDesignation(),
66
+ });
67
+ });
68
+
69
+ const backBtn = wrap.querySelector('button[data-action="back"]');
70
+ if (backBtn && typeof onBack === "function") {
71
+ backBtn.addEventListener("click", onBack);
72
+ }
73
+
74
+ return wrap;
75
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Shared Org Fields (Team, Sub-team, Designation) Dropdowns Component
3
+ * Handles dynamic fetching from /org/structure and cascaded sub-team filtering.
4
+ */
5
+
6
+ function escapeHtml(str) {
7
+ if (!str) return "";
8
+ return String(str)
9
+ .replace(/&/g, "&amp;")
10
+ .replace(/</g, "&lt;")
11
+ .replace(/>/g, "&gt;")
12
+ .replace(/"/g, "&quot;");
13
+ }
14
+
15
+ export function renderOrgFieldsHtml({
16
+ showTeam = true,
17
+ showSubTeam = true,
18
+ showDesignation = true,
19
+ prefix = "wf-",
20
+ } = {}) {
21
+ let html = "";
22
+
23
+ if (showTeam && showSubTeam) {
24
+ html += `
25
+ <div class="wf-row-2">
26
+ <div class="wf-field">
27
+ <label class="wf-label" for="${prefix}team">Team</label>
28
+ <select id="${prefix}team" class="wf-input wf-select" autocomplete="organization">
29
+ <option value="">Select Team</option>
30
+ </select>
31
+ </div>
32
+ <div class="wf-field">
33
+ <label class="wf-label" for="${prefix}sub-team">Sub-team</label>
34
+ <select id="${prefix}sub-team" class="wf-input wf-select" disabled>
35
+ <option value="">Select a team first</option>
36
+ </select>
37
+ </div>
38
+ </div>
39
+ `;
40
+ } else if (showTeam) {
41
+ html += `
42
+ <div class="wf-field">
43
+ <label class="wf-label" for="${prefix}team">Team</label>
44
+ <select id="${prefix}team" class="wf-input wf-select" autocomplete="organization">
45
+ <option value="">Select Team</option>
46
+ </select>
47
+ </div>
48
+ `;
49
+ } else if (showSubTeam) {
50
+ html += `
51
+ <div class="wf-field">
52
+ <label class="wf-label" for="${prefix}sub-team">Sub-team</label>
53
+ <select id="${prefix}sub-team" class="wf-input wf-select" disabled>
54
+ <option value="">Select a team first</option>
55
+ </select>
56
+ </div>
57
+ `;
58
+ }
59
+
60
+ if (showDesignation) {
61
+ html += `
62
+ <div class="wf-field">
63
+ <label class="wf-label" for="${prefix}designation">Designation</label>
64
+ <select id="${prefix}designation" class="wf-input wf-select" autocomplete="organization-title">
65
+ <option value="">Select Designation</option>
66
+ </select>
67
+ </div>
68
+ `;
69
+ }
70
+
71
+ return html;
72
+ }
73
+
74
+ export function attachOrgFieldsLogic({
75
+ wrap,
76
+ prefix = "wf-",
77
+ prefill = {},
78
+ options = {},
79
+ }) {
80
+ const teamEl = wrap.querySelector(`#${prefix}team`);
81
+ const subTeamEl = wrap.querySelector(`#${prefix}sub-team`);
82
+ const designationEl = wrap.querySelector(`#${prefix}designation`);
83
+
84
+ let currentTeams = [];
85
+ let currentDesignations = [];
86
+
87
+ function updateSubTeams(selectedTeamName, selectedSubTeamValue = "") {
88
+ if (!subTeamEl) return;
89
+ if (!selectedTeamName) {
90
+ subTeamEl.innerHTML = '<option value="">Select a team first</option>';
91
+ subTeamEl.disabled = true;
92
+ subTeamEl.value = "";
93
+ return;
94
+ }
95
+
96
+ const team = currentTeams.find((t) => t.name === selectedTeamName || t.id === selectedTeamName);
97
+ const subTeams = team && Array.isArray(team.sub_teams) ? team.sub_teams : [];
98
+
99
+ if (subTeams.length === 0) {
100
+ subTeamEl.innerHTML = '<option value="">No sub-teams</option>';
101
+ subTeamEl.disabled = true;
102
+ subTeamEl.value = "";
103
+ return;
104
+ }
105
+
106
+ let opts = '<option value="">Select Sub-team</option>';
107
+ subTeams.forEach((st) => {
108
+ opts += `<option value="${escapeHtml(st.name)}">${escapeHtml(st.name)}</option>`;
109
+ });
110
+ subTeamEl.innerHTML = opts;
111
+ subTeamEl.disabled = false;
112
+
113
+ if (selectedSubTeamValue) {
114
+ subTeamEl.value = selectedSubTeamValue;
115
+ }
116
+ }
117
+
118
+ function populate(orgData) {
119
+ if (!orgData) return;
120
+ currentTeams = Array.isArray(orgData.teams) ? orgData.teams : [];
121
+ currentDesignations = Array.isArray(orgData.designations) ? orgData.designations : [];
122
+
123
+ // Populate Teams
124
+ if (teamEl) {
125
+ const prevVal = teamEl.value || prefill.team || "";
126
+ let opts = '<option value="">Select Team</option>';
127
+ currentTeams.forEach((t) => {
128
+ opts += `<option value="${escapeHtml(t.name)}">${escapeHtml(t.name)}</option>`;
129
+ });
130
+ teamEl.innerHTML = opts;
131
+ if (prevVal) {
132
+ teamEl.value = prevVal;
133
+ updateSubTeams(prevVal, subTeamEl?.value || prefill.subTeam || "");
134
+ }
135
+ }
136
+
137
+ // Populate Designations
138
+ if (designationEl) {
139
+ const prevVal = designationEl.value || prefill.designation || "";
140
+ let opts = '<option value="">Select Designation</option>';
141
+ currentDesignations.forEach((d) => {
142
+ opts += `<option value="${escapeHtml(d.name)}">${escapeHtml(d.name)}</option>`;
143
+ });
144
+ designationEl.innerHTML = opts;
145
+ if (prevVal) {
146
+ designationEl.value = prevVal;
147
+ }
148
+ }
149
+ }
150
+
151
+ // Handle Team change -> cascade Sub-teams
152
+ if (teamEl) {
153
+ teamEl.addEventListener("change", () => {
154
+ updateSubTeams(teamEl.value);
155
+ });
156
+ }
157
+
158
+ // If org structure was already loaded, populate immediately
159
+ if (options.orgStructure) {
160
+ populate(options.orgStructure);
161
+ }
162
+
163
+ // If api is available, ensure we have the latest org structure
164
+ if (options.api && typeof options.api.getOrgStructure === "function") {
165
+ const { entityId, entityName, entityDomain, onOrgLoaded } = options;
166
+ if (entityId || entityName || entityDomain) {
167
+ options.api.getOrgStructure({ entityId, entityName, entityDomain })
168
+ .then((res) => {
169
+ if (res && (Array.isArray(res.teams) || Array.isArray(res.designations))) {
170
+ if (typeof onOrgLoaded === "function") onOrgLoaded(res);
171
+ populate(res);
172
+ }
173
+ })
174
+ .catch(() => {});
175
+ }
176
+ }
177
+
178
+ return {
179
+ getTeam() {
180
+ return teamEl ? teamEl.value.trim() : "";
181
+ },
182
+ getSubTeam() {
183
+ return subTeamEl && !subTeamEl.disabled ? subTeamEl.value.trim() : "";
184
+ },
185
+ getDesignation() {
186
+ return designationEl ? designationEl.value.trim() : "";
187
+ },
188
+ populate,
189
+ };
190
+ }