central-auth-js-sdk 1.0.13 → 1.0.14

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.
@@ -13,7 +13,13 @@ npm install central-auth-js-sdk
13
13
  Use in your app (with a bundler such as Vite, Webpack, or Parcel):
14
14
 
15
15
  ```javascript
16
- import { initializeAuth, startSignup, startLogin } from 'central-auth-js-sdk';
16
+ import {
17
+ initializeAuth,
18
+ startSignup,
19
+ startLogin,
20
+ autoResumeOAuthFlow,
21
+ hasPendingOAuthRedirect,
22
+ } from 'central-auth-js-sdk';
17
23
  import 'central-auth-js-sdk/styles.css';
18
24
  ```
19
25
 
@@ -37,6 +43,8 @@ You need:
37
43
  2. The **SDK CSS** loaded (so the UI looks correct).
38
44
  3. A call to **`startSignup`** or **`startLogin`** with at least `container` and **`env`** (which backend to use: `local`, `dev`, `staging`, or `prod`). **`appIdentifier`** is optional; include it only if your backend expects it for OAuth or login APIs.
39
45
 
46
+ For **Google/Microsoft OAuth**, also call **`initializeAuth`** once with **`defaultContainer`** and **`autoResumeOAuth: true`** (default). After the user returns from the provider, the SDK mounts the correct step automatically — see [§ 12 OAuth redirect & auto-resume](#12-oauth-redirect--auto-resume).
47
+
40
48
  **Minimal signup example:**
41
49
 
42
50
  ```html
@@ -76,27 +84,37 @@ You need:
76
84
 
77
85
  | Function | Purpose |
78
86
  |----------|--------|
79
- | **`initializeAuth(config)`** | (Optional) Set default config once; `startSignup` / `startLogin` can override per call. |
87
+ | **`initializeAuth(config)`** | (Optional) Set default config once; enables OAuth auto-resume when `defaultContainer` is set. |
80
88
  | **`startSignup(config)`** | Run the sign-up flow in the given container. |
81
89
  | **`startLogin(config)`** | Run the login flow in the given container. |
90
+ | **`autoResumeOAuthFlow(config)`** | After Google/Microsoft redirect, mount login or signup from URL params. No-op if URL has no OAuth params. |
91
+ | **`hasPendingOAuthRedirect()`** | Returns `true` when the current URL contains OAuth callback query params. |
92
+
93
+ Lower-level helpers (optional): `parseOAuthRedirect()`, `getOAuthRedirectFlowType()`, `markOAuthRedirectPending()`, `OAUTH_PENDING_CLASS`.
82
94
 
83
- You can use **only** `startSignup` and `startLogin` and pass full config each time. Or call **`initializeAuth`** once and then call **`auth.startSignup({})`** / **`auth.startLogin({})`** so they use the stored defaults.
95
+ You can use **only** `startSignup` and `startLogin` and pass full config each time. Or call **`initializeAuth`** once and then call **`auth.startSignup({})`** / **`auth.startLogin({})`** so they use the stored defaults. For social login, prefer **`initializeAuth`** with **`autoResumeOAuth: true`** so the post-OAuth page reload is handled automatically.
84
96
 
85
97
  ---
86
98
 
87
99
  ## 4. initializeAuth(config)
88
100
 
89
- Call once (e.g. at app load) to set defaults. Returns `{ startSignup, startLogin, getConfig }`.
101
+ Call once (e.g. at app load) to set defaults. Returns `{ startSignup, startLogin, autoResumeOAuthFlow, hasPendingOAuthRedirect, getConfig }`.
102
+
103
+ When **`autoResumeOAuth`** is `true` (default) and **`defaultContainer`** is set, the SDK automatically calls **`autoResumeOAuthFlow`** on page load if the URL contains OAuth callback params (e.g. after Google redirects back).
90
104
 
91
105
  | Parameter | Type | Default | Description |
92
106
  |-----------|------|---------|-------------|
93
107
  | `env` | string | `'local'` | Environment: `'local'` \| `'dev'` \| `'staging'` \| `'prod'`. SDK uses the predefined API URL for that env. |
94
108
  | `appIdentifier` | string | — | Optional. When provided, sent with OAuth and login-related API requests; omit to not send `app_identifier`. |
95
- | `defaultContainer` | string \| HTMLElement | — | Default container; can override in `startSignup` / `startLogin` |
109
+ | `defaultContainer` | string \| HTMLElement | — | Default container; can override in `startSignup` / `startLogin`. **Required for OAuth auto-resume.** |
110
+ | `autoResumeOAuth` | boolean | `true` | When `true` and `defaultContainer` is set, auto-mount login/signup after OAuth redirect. |
96
111
  | `loginMethods` | object | — | Default login methods (see Login config) |
97
112
  | `frsAsSecondFactor` | boolean | `false` | Default: require face verification after primary auth |
98
113
  | `enableFaceRegistration` | boolean | `true` | Whether sign-up includes face registration step |
114
+ | `onContinue` | function | — | Default callback when user clicks Continue on success (see [§ 9](#9-returning-to-the-parent-application)) |
99
115
  | `onBack` | function | — | Default callback when user clicks ← Back on the first screen of signup/login (see [§ 9](#9-returning-to-the-parent-application)) |
116
+ | `onLoginComplete` | function | — | Default login-complete callback |
117
+ | `onSignupComplete` | function | — | Default signup-complete callback |
100
118
 
101
119
  **Environments:** The SDK uses only `env` to choose the API base URL:
102
120
 
@@ -116,12 +134,15 @@ const auth = initializeAuth({
116
134
  env: 'dev', // uses https://central-auth-dev.soham.ai/apis
117
135
  appIdentifier: 'my-app',
118
136
  defaultContainer: '#auth-container',
137
+ autoResumeOAuth: true,
119
138
  loginMethods: {
120
139
  emailPassword: true,
121
140
  emailOtp: true,
122
141
  social: { google: true, microsoft: true },
123
142
  },
124
143
  frsAsSecondFactor: true,
144
+ onContinue: goToApp,
145
+ onBack: goToApp,
125
146
  });
126
147
 
127
148
  // Later:
@@ -268,7 +289,7 @@ Starts the **login flow**. If the parent enables **only one** login method, the
268
289
  | `env` | string | no | Environment: `'local'` \| `'dev'` \| `'staging'` \| `'prod'` (default from init or `'local'`) |
269
290
  | `appIdentifier` | string | no | Optional. When provided, sent with login/OAuth API requests; omit to not send. |
270
291
  | **`loginMethods`** | object | no | Which login methods are enabled (see below) |
271
- | `frsAsSecondFactor` | boolean | no | Require face verification after password/OTP/social |
292
+ | `frsAsSecondFactor` | boolean | no | Require face verification after password/OTP/social (client-side). Domain-based 2FA from the backend is handled automatically — see [§ 12](#12-oauth-redirect--auto-resume). |
272
293
  | **`onSignUp()`** | function | no | Called when user clicks **Sign up** on the login screen (e.g. unmount login and start signup in the **same container**). |
273
294
  | **`onLoginComplete(payload)`** | function | no | Called **once** when login completes. `payload`: `identifier` + any fields from final API (e.g. `access_token`, `refresh_token`). |
274
295
  | **`onContinue()`** | function | no | Called when user clicks **Continue to application** on the success screen. |
@@ -288,6 +309,13 @@ Starts the **login flow**. If the parent enables **only one** login method, the
288
309
 
289
310
  **Email + FRS:** On the face capture screen, if the user has no webcam, they can click **“No webcam? Sign in with OTP instead”**. The SDK sends an OTP to the email they entered and switches to the OTP step so they can complete login without face capture. No extra configuration is required.
290
311
 
312
+ **Phone face capture:** If the user has no webcam, they can click **“Use phone instead”** on the face screen. The SDK shows a QR code + PIN and polls the backend until a face image is captured on the phone.
313
+
314
+ **Backend-driven 2FA (face as second factor):** When your backend requires face verification (e.g. domain policy for `@gmail.com`), it returns `two_factor_required: true` and a short-lived `fr_token`. The SDK shows the face capture screen and submits the image with `fr_token` to complete login. This applies to:
315
+
316
+ - **Password / OTP login** — response from `POST /login/custom` or `POST /otp/verify`
317
+ - **Google / Microsoft login** — query params on the OAuth redirect URL (see [§ 12](#12-oauth-redirect--auto-resume))
318
+
291
319
  **Example — single method (no method picker):**
292
320
 
293
321
  ```javascript
@@ -376,7 +404,8 @@ instance.unmount();
376
404
  ## 8. Getting tokens and details back
377
405
 
378
406
  - **Signup:** Pass **`onSignupComplete(payload)`**. The SDK calls it once when signup finishes. `payload` includes `email`, `mobile`, `userId` and whatever your **final signup API** returns (e.g. `access_token`, `refresh_token`). Your backend should return these in the final signup response (e.g. `POST /signup/fr/enroll`).
379
- - **Login:** Pass **`onLoginComplete(payload)`**. The SDK calls it once when login finishes. `payload` includes `identifier` and whatever your **final login API** returns (e.g. `access_token`, `refresh_token`). Your backend should return these in the final login response (e.g. `POST /login/custom`, `POST /otp/verify`, etc.).
407
+ - **Login:** Pass **`onLoginComplete(payload)`**. The SDK calls it once when login finishes. `payload` includes `identifier` and whatever your **final login API** returns (e.g. `access_token`, `refresh_token`, `token`). Your backend should return these in the final login response (e.g. `POST /login/custom`, `POST /otp/verify`, `POST /login/google/fr/enroll`, etc.).
408
+ - **OAuth login complete:** When the backend redirects with `token=...` (JWT) and no further steps, the SDK lands on the success screen and includes `token`, `email`, `user_id`, etc. in the payload passed to **`onLoginComplete`**.
380
409
 
381
410
  You can also read **`instance.getState()`** after the flow completes; it includes **`signupResult`** or **`loginResult`** (the raw final API response).
382
411
 
@@ -462,80 +491,67 @@ Returning to the parent app is **not automatic** beyond SDK unmount. Integrators
462
491
  ## 10. Full integration example
463
492
 
464
493
  ```javascript
465
- import { initializeAuth, startSignup, startLogin } from 'central-auth-js-sdk';
494
+ import { initializeAuth } from 'central-auth-js-sdk';
466
495
  import 'central-auth-js-sdk/styles.css';
467
496
 
497
+ let currentInstance = null;
498
+
499
+ function goToApp() {
500
+ if (currentInstance) currentInstance.unmount();
501
+ currentInstance = null;
502
+ document.getElementById('auth-container').style.display = 'none';
503
+ document.getElementById('app-root').style.display = 'block';
504
+ }
505
+
506
+ function saveToken(token) {
507
+ if (token) localStorage.setItem('access_token', token);
508
+ }
509
+
468
510
  const auth = initializeAuth({
469
511
  env: 'dev',
470
512
  appIdentifier: 'my-app',
471
513
  defaultContainer: '#auth-container',
514
+ autoResumeOAuth: true, // auto-mount after Google/Microsoft redirect
472
515
  loginMethods: {
473
516
  emailPassword: true,
474
517
  emailOtp: true,
475
518
  social: { google: true, microsoft: true },
476
519
  emailFrs: true,
477
520
  },
478
- frsAsSecondFactor: false,
521
+ onLoginComplete(payload) {
522
+ saveToken(payload.token || payload.access_token);
523
+ },
524
+ onSignupComplete(payload) {
525
+ saveToken(payload.access_token);
526
+ },
527
+ onContinue: goToApp,
528
+ onBack: goToApp,
479
529
  });
480
530
 
481
- let currentInstance = null;
482
-
483
- function goToApp() {
484
- if (currentInstance) currentInstance.unmount();
485
- currentInstance = null;
486
- document.getElementById('auth-container').style.display = 'none';
487
- document.getElementById('app-root').style.display = 'block';
488
- }
489
-
490
531
  function showSignup() {
491
532
  if (currentInstance) currentInstance.unmount();
492
533
  document.getElementById('auth-container').style.display = 'block';
534
+ document.getElementById('app-root').style.display = 'none';
493
535
  currentInstance = auth.startSignup({
494
- onSignupComplete(payload) {
495
- saveToken(payload.access_token);
496
- goToApp();
497
- },
498
- onContinue: goToApp,
499
- onBack: goToApp,
500
- onSwitchToLogin: showLoginFrs,
501
- });
502
- }
503
-
504
- function showLoginFrs() {
505
- if (currentInstance) currentInstance.unmount();
506
- document.getElementById('auth-container').style.display = 'block';
507
- currentInstance = auth.startLogin({
508
- loginMethods: { emailFrs: true },
509
- onLoginComplete(payload) {
510
- saveToken(payload.access_token);
511
- goToApp();
512
- },
513
- onContinue: goToApp,
514
- onBack: goToApp,
515
- onSignUp: showSignup,
536
+ onSwitchToLogin: () => showLogin({ emailFrs: true }),
516
537
  });
517
538
  }
518
539
 
519
540
  function showLogin(loginMethods = { emailPassword: true }) {
520
541
  if (currentInstance) currentInstance.unmount();
521
542
  document.getElementById('auth-container').style.display = 'block';
543
+ document.getElementById('app-root').style.display = 'none';
522
544
  currentInstance = auth.startLogin({
523
545
  loginMethods,
524
- onLoginComplete(payload) {
525
- saveToken(payload.access_token);
526
- goToApp();
527
- },
528
- onContinue: goToApp,
529
- onBack: goToApp,
530
546
  onSignUp: showSignup,
531
547
  });
532
548
  }
533
549
 
534
- function saveToken(token) {
535
- if (token) localStorage.setItem('access_token', token);
550
+ // If OAuth redirect params are present, initializeAuth already resumed the flow.
551
+ // Otherwise show your normal entry point:
552
+ if (!auth.hasPendingOAuthRedirect()) {
553
+ showLogin();
536
554
  }
537
-
538
- showLogin();
539
555
  ```
540
556
 
541
557
  ---
@@ -544,8 +560,137 @@ showLogin();
544
560
 
545
561
  - **CORS:** Your API must allow requests from your app’s origin (e.g. `Access-Control-Allow-Origin`).
546
562
  - **Signup:** The SDK calls your signup endpoints (e.g. `/signup/start`, `/signup/email/verify`, `/signup/fr/enroll`). The **final** response can include `access_token`, `refresh_token`, etc.; the SDK forwards them in `onSignupComplete(payload)` and `getState().signupResult`.
547
- - **Login:** The SDK calls your login endpoints (e.g. `/login/custom`, `/otp/verify`, `/login/email/fr`). The **final** response can include tokens; the SDK forwards them in `onLoginComplete(payload)` and `getState().loginResult`.
548
- - **OAuth:** For Google/Microsoft, configure your backend with the same redirect URI your app uses. The SDK reads `login_token` and `provider` from the URL after redirect and continues the flow.
563
+ - **Login:** The SDK calls your login endpoints (e.g. `/login/custom`, `/otp/verify`, `/login/email/fr`, `/login/google/fr/enroll`). The **final** response can include tokens; the SDK forwards them in `onLoginComplete(payload)` and `getState().loginResult`.
564
+ - **2FA (face verification):** When face verification is required, the backend returns (JSON body or OAuth redirect query params):
565
+
566
+ ```json
567
+ {
568
+ "success": true,
569
+ "two_factor_required": true,
570
+ "next_step": "FR_VERIFICATION",
571
+ "login_token": "...",
572
+ "fr_token": "...",
573
+ "fr_token_expires_in_s": 300,
574
+ "message": "Facial recognition verification required"
575
+ }
576
+ ```
577
+
578
+ The SDK shows the face capture screen and submits `fr_token` + face image to the appropriate endpoint (e.g. `POST /login/google/fr/enroll` for social, `POST /login/custom` with `fr_image` for password login).
579
+ - **OAuth:** Start Google/Microsoft login with `redirect_uri` pointing to the page where your SDK loads. After auth, the backend **302-redirects** to that URL with query params — see [§ 12](#12-oauth-redirect--auto-resume). The SDK reads those params and continues the flow (face verification, success, or error). Same behavior for Microsoft OAuth.
549
580
  - **app_identifier:** When the parent app passes **`appIdentifier`**, the SDK sends it as `app_identifier` on login-related requests (e.g. `/login/custom`, `/login/email/fr`, OAuth start URLs, social mobile/FR). If the parent omits it, the SDK does not send the parameter.
550
581
 
551
582
  For endpoint details and request/response shapes, see **API_INTEGRATION.md** and your backend docs.
583
+
584
+ ---
585
+
586
+ ## 12. OAuth redirect & auto-resume
587
+
588
+ Google and Microsoft login use a full-page redirect. When the user returns, the browser reloads your app with query params on the URL. **The SDK handles parsing, routing, and cleanup** — you do not need custom “if URL has `login_token` then …” logic in your app.
589
+
590
+ ### What the parent app must do
591
+
592
+ 1. Call **`initializeAuth({ defaultContainer, autoResumeOAuth: true, ... })`** once at app load, **or** call **`autoResumeOAuthFlow({ container, ... })`** when your app boots.
593
+ 2. Pass the same **`redirect_uri`** to OAuth start that your app uses as the return page (the SDK builds this as `window.location.origin + window.location.pathname` when the user clicks Google/Microsoft).
594
+ 3. Provide **`onContinue`**, **`onBack`**, and optionally **`onLoginComplete`** callbacks.
595
+
596
+ The SDK does **not** navigate your main app UI automatically. Hide/show your app shell in those callbacks (see [§ 9](#9-returning-to-the-parent-application)).
597
+
598
+ ### Optional: reduce “home screen flash” after OAuth
599
+
600
+ When OAuth params are detected, the SDK adds **`central-auth-oauth-pending`** to `<html>`. Hide your main UI while the SDK loads:
601
+
602
+ ```css
603
+ html.central-auth-oauth-pending [data-central-auth-main] {
604
+ display: none !important;
605
+ }
606
+ html.central-auth-oauth-pending [data-central-auth-sdk] {
607
+ display: flex !important;
608
+ }
609
+ ```
610
+
611
+ Mark your main content with `data-central-auth-main` and your auth container wrapper with `data-central-auth-sdk`.
612
+
613
+ For the earliest possible switch (before the SDK module loads), you can add a tiny inline script in `<head>` that sets the same class when OAuth params are present — the demo app uses this pattern.
614
+
615
+ ### Backend redirect examples
616
+
617
+ After Google auth, the backend redirects to your `redirect_uri` with query params:
618
+
619
+ **Login complete:**
620
+
621
+ ```
622
+ https://yourapp.com/auth?success=true&provider=google&token=eyJ...&user_id=...&email=...&entity_domain=gmail.com&message=Login+successful
623
+ ```
624
+
625
+ **2FA — face verification required:**
626
+
627
+ ```
628
+ https://yourapp.com/auth?success=true&two_factor_required=true&provider=google&next_step=FR_VERIFICATION&login_token=...&fr_token=...&fr_token_expires_in_s=300&message=Facial+recognition+verification+required&user_id=...&entity_domain=gmail.com
629
+ ```
630
+
631
+ **Signup — face enrollment required:**
632
+
633
+ ```
634
+ https://yourapp.com/auth?success=true&provider=google&next_step=FR_ENROLLMENT&login_token=...&user_id=...&entity_domain=gmail.com&message=Facial+recognition+enrollment+required
635
+ ```
636
+
637
+ **Signup — mobile verification:**
638
+
639
+ ```
640
+ https://yourapp.com/auth?success=true&provider=google&next_step=MOBILE_VERIFICATION&login_token=...&user_id=...
641
+ ```
642
+
643
+ **Error:**
644
+
645
+ ```
646
+ https://yourapp.com/auth?success=false&provider=google&error=Entity+not+found+for+email+domain+gmail.com
647
+ ```
648
+
649
+ ### How the SDK routes each case
650
+
651
+ | Redirect | SDK action |
652
+ |----------|------------|
653
+ | `two_factor_required=true` or `next_step=FR_VERIFICATION` | Login flow → **face capture** → `POST /login/{google\|microsoft}/fr/enroll` with `login_token`, `fr_token`, `fr_image` |
654
+ | `token=...` (login complete) | Login flow → **success screen** → `onLoginComplete` with JWT |
655
+ | `next_step=FR_ENROLLMENT` | Signup flow → **face enrollment** |
656
+ | `next_step=MOBILE_VERIFICATION` | Signup flow → **mobile entry** |
657
+ | `success=false` or `error=...` | Login flow → show error banner |
658
+ | Params cleaned from URL | SDK removes OAuth query params via `history.replaceState` (no reload) |
659
+
660
+ ### End-to-end flow (Google login + 2FA)
661
+
662
+ ```
663
+ User clicks "Continue with Google"
664
+ → GET /login/google/start?redirect_uri=...&app_identifier=...
665
+ → Google OAuth
666
+ → Backend 302 to your app with two_factor_required=true, login_token, fr_token, ...
667
+ → SDK auto-resumes (face capture screen)
668
+ → User captures face
669
+ → POST /login/google/fr/enroll
670
+ → Success screen
671
+ → User clicks Continue → onContinue() → your app
672
+ ```
673
+
674
+ ### Manual auto-resume (without initializeAuth)
675
+
676
+ ```javascript
677
+ import { autoResumeOAuthFlow, hasPendingOAuthRedirect } from 'central-auth-js-sdk';
678
+
679
+ if (hasPendingOAuthRedirect()) {
680
+ const resumed = autoResumeOAuthFlow({
681
+ container: '#auth-container',
682
+ env: 'dev',
683
+ appIdentifier: 'my-app',
684
+ onContinue: goToApp,
685
+ onBack: goToApp,
686
+ });
687
+ if (resumed) {
688
+ currentInstance = resumed.instance;
689
+ // resumed.flow is 'login' or 'signup'
690
+ }
691
+ }
692
+ ```
693
+
694
+ ### Persist `env` across OAuth reload
695
+
696
+ OAuth causes a **full page reload**, so in-memory state is lost. Store `env` (and `appIdentifier` if needed) in **`sessionStorage`** before redirecting to Google, and pass the same values to **`autoResumeOAuthFlow`** or **`initializeAuth`**. The demo app uses `sessionStorage` for this.
package/auth.js CHANGED
@@ -4,12 +4,23 @@
4
4
  */
5
5
 
6
6
  import { resolveApiBaseUrl } from './core/env-config.js';
7
+ import {
8
+ hasOAuthRedirect,
9
+ parseOAuthRedirect,
10
+ getOAuthRedirectFlowType,
11
+ markOAuthRedirectPending,
12
+ clearOAuthRedirectPendingMark,
13
+ } from './core/oauth-redirect.js';
7
14
  import { mount } from './signup.js';
8
15
  import { mountLogin } from './login.js';
9
16
 
10
17
  /** Default configuration stored after initializeAuth() */
11
18
  let defaultConfig = {};
12
19
 
20
+ const DEFAULT_SOCIAL_LOGIN_METHODS = {
21
+ social: { google: true, microsoft: true },
22
+ };
23
+
13
24
  /**
14
25
  * Merge config with defaults. Passed config overrides defaults.
15
26
  * @param {Object} overrides - config passed to startSignup / startLogin
@@ -42,38 +53,99 @@ function resolveContainer(container) {
42
53
  return el && el.nodeType === 1 ? el : null;
43
54
  }
44
55
 
56
+ /**
57
+ * Resume login or signup automatically after an OAuth redirect.
58
+ * No-op when the URL has no OAuth callback params.
59
+ *
60
+ * @param {Object} [config] - Merged with initializeAuth defaults
61
+ * @param {string|HTMLElement} [config.container] - Required unless defaultContainer was set in initializeAuth
62
+ * @returns {{ flow: 'login'|'signup', instance: Object }|null}
63
+ */
64
+ export function autoResumeOAuthFlow(config = {}) {
65
+ if (!hasOAuthRedirect()) return null;
66
+
67
+ markOAuthRedirectPending();
68
+
69
+ const merged = mergeConfig(config);
70
+ const container = config.container ?? merged.defaultContainer;
71
+ const el = resolveContainer(container);
72
+ if (!el) {
73
+ console.warn('[Central Auth SDK] autoResumeOAuthFlow: container is required (pass config.container or set defaultContainer in initializeAuth)');
74
+ return null;
75
+ }
76
+
77
+ const redirect = parseOAuthRedirect();
78
+ const flowType = getOAuthRedirectFlowType(redirect) || 'login';
79
+
80
+ let instance = null;
81
+ if (flowType === 'signup') {
82
+ instance = startSignup({ ...config, container: el });
83
+ } else {
84
+ instance = startLogin({
85
+ ...config,
86
+ container: el,
87
+ loginMethods: config.loginMethods ?? merged.loginMethods ?? DEFAULT_SOCIAL_LOGIN_METHODS,
88
+ });
89
+ }
90
+
91
+ if (instance) {
92
+ clearOAuthRedirectPendingMark();
93
+ return { flow: flowType, instance };
94
+ }
95
+ return null;
96
+ }
97
+
45
98
  /**
46
99
  * Initialize the SDK with optional default configuration.
47
100
  * Call this once (e.g. at app load) to set shared defaults; startSignup/startLogin can override per call.
48
101
  *
102
+ * When `autoResumeOAuth` is true (default) and `defaultContainer` is set, the SDK automatically
103
+ * mounts the correct flow after Google/Microsoft OAuth redirects back to your app.
104
+ *
49
105
  * @param {Object} config - Default static configuration
50
106
  * @param {string} [config.env='local'] - Environment: 'local' | 'dev' | 'staging' | 'prod' — SDK uses the predefined API URL for that env
51
107
  * @param {string} [config.appIdentifier] - Optional app identifier for OAuth / custom login; only sent when provided
52
108
  * @param {string|HTMLElement} [config.defaultContainer] - Default container for rendering (can be overridden in start*)
109
+ * @param {boolean} [config.autoResumeOAuth=true] - Auto-mount login/signup when OAuth redirect params are in the URL
53
110
  * @param {Object} [config.loginMethods] - Default enabled login methods (see startLogin)
54
111
  * @param {boolean} [config.frsAsSecondFactor] - Default for FRS as second factor (see startLogin)
55
112
  * @param {boolean} [config.enableFaceRegistration=true] - Whether sign-up flow includes face registration step
56
113
  * @param {Object} [config.theme] - Optional theme overrides (primaryColor, secondaryColor, backgroundColor, backgroundImage, cardColor, textColor, fontFamily, mode)
57
- * @returns {{ startSignup: Function, startLogin: Function, getConfig: Function }}
114
+ * @returns {{ startSignup: Function, startLogin: Function, autoResumeOAuthFlow: Function, hasPendingOAuthRedirect: Function, getConfig: Function }}
58
115
  */
59
116
  export function initializeAuth(config = {}) {
60
117
  defaultConfig = {
61
118
  env: 'local',
62
119
  appIdentifier: undefined,
63
120
  defaultContainer: null,
121
+ autoResumeOAuth: true,
64
122
  loginMethods: null,
65
123
  frsAsSecondFactor: false,
66
124
  enableFaceRegistration: true,
67
125
  theme: null,
68
126
  ...config,
69
127
  };
70
- return {
71
- startSignup: startSignup,
72
- startLogin: startLogin,
128
+
129
+ const api = {
130
+ startSignup,
131
+ startLogin,
132
+ autoResumeOAuthFlow,
133
+ hasPendingOAuthRedirect: hasOAuthRedirect,
73
134
  getConfig() {
74
135
  return { ...defaultConfig };
75
136
  },
76
137
  };
138
+
139
+ if (defaultConfig.autoResumeOAuth !== false && defaultConfig.defaultContainer && hasOAuthRedirect()) {
140
+ const run = () => { autoResumeOAuthFlow({}); };
141
+ if (typeof document !== 'undefined' && document.readyState === 'loading') {
142
+ document.addEventListener('DOMContentLoaded', run, { once: true });
143
+ } else {
144
+ run();
145
+ }
146
+ }
147
+
148
+ return api;
77
149
  }
78
150
 
79
151
  /**
@@ -100,6 +172,7 @@ export function startSignup(config = {}) {
100
172
  return null;
101
173
  }
102
174
  const options = {
175
+ env: merged.env,
103
176
  apiBaseUrl: resolveApiBaseUrl(merged),
104
177
  appIdentifier: merged.appIdentifier,
105
178
  enableFaceRegistration: merged.enableFaceRegistration,
@@ -145,6 +218,7 @@ export function startLogin(config = {}) {
145
218
  return null;
146
219
  }
147
220
  const options = {
221
+ env: merged.env,
148
222
  apiBaseUrl: resolveApiBaseUrl(merged),
149
223
  appIdentifier: merged.appIdentifier,
150
224
  loginMethods: merged.loginMethods,
package/core/api.js CHANGED
@@ -116,12 +116,13 @@ export function createApi(baseUrl = DEFAULT_BASE, options = {}) {
116
116
  return request('POST', '/login/google/mobile/verify', body);
117
117
  },
118
118
 
119
- async googleFrEnroll(loginToken, frImageFile1, frImageFile2 = null, appIdentifier) {
119
+ async googleFrEnroll(loginToken, frImageFile1, frImageFile2 = null, appIdentifier, frToken = null) {
120
120
  const fd = new FormData();
121
121
  fd.append('login_token', loginToken);
122
122
  fd.append('fr_image', frImageFile1);
123
123
  fd.append('fr_image2', frImageFile2 || frImageFile1);
124
124
  if (appIdentifier) fd.append('app_identifier', appIdentifier);
125
+ if (frToken) fd.append('fr_token', frToken);
125
126
  return request('POST', '/login/google/fr/enroll', fd);
126
127
  },
127
128
 
@@ -137,19 +138,20 @@ export function createApi(baseUrl = DEFAULT_BASE, options = {}) {
137
138
  return request('POST', '/login/microsoft/mobile/verify', body);
138
139
  },
139
140
 
140
- async microsoftFrEnroll(loginToken, frImageFile1, frImageFile2 = null, appIdentifier) {
141
+ async microsoftFrEnroll(loginToken, frImageFile1, frImageFile2 = null, appIdentifier, frToken = null) {
141
142
  const fd = new FormData();
142
143
  fd.append('login_token', loginToken);
143
144
  fd.append('fr_image', frImageFile1);
144
145
  fd.append('fr_image2', frImageFile2 || frImageFile1);
145
146
  if (appIdentifier) fd.append('app_identifier', appIdentifier);
147
+ if (frToken) fd.append('fr_token', frToken);
146
148
  return request('POST', '/login/microsoft/fr/enroll', fd);
147
149
  },
148
150
 
149
151
  // --- Login (custom, OTP, email+FR) ---
150
152
 
151
153
  /** POST /login/custom — Email or mobile + password; optional fr_image, fr_image2 for FRS. app_identifier only when provided. */
152
- async loginCustom(identifier, password, appIdentifier, frImageFiles = null) {
154
+ async loginCustom(identifier, password, appIdentifier, frImageFiles = null, frToken = null) {
153
155
  const body = { identifier, password };
154
156
  if (appIdentifier) body.app_identifier = appIdentifier;
155
157
  if (frImageFiles && frImageFiles.length) {
@@ -157,6 +159,7 @@ export function createApi(baseUrl = DEFAULT_BASE, options = {}) {
157
159
  fd.append('identifier', identifier);
158
160
  fd.append('password', password);
159
161
  if (appIdentifier) fd.append('app_identifier', appIdentifier);
162
+ if (frToken) fd.append('fr_token', frToken);
160
163
  fd.append('fr_image', frImageFiles[0]);
161
164
  fd.append('fr_image2', frImageFiles[1] || frImageFiles[0]);
162
165
  return request('POST', '/login/custom', fd);
@@ -170,13 +173,14 @@ export function createApi(baseUrl = DEFAULT_BASE, options = {}) {
170
173
  },
171
174
 
172
175
  /** POST /otp/verify — optional frImageFiles for 2FA */
173
- async otpVerify(identifier, domain, channel, otp, frImageFiles = null) {
176
+ async otpVerify(identifier, domain, channel, otp, frImageFiles = null, frToken = null) {
174
177
  if (frImageFiles && frImageFiles.length) {
175
178
  const fd = new FormData();
176
179
  fd.append('identifier', identifier);
177
180
  if (domain) fd.append('domain', domain);
178
181
  fd.append('channel', channel);
179
182
  fd.append('otp', otp);
183
+ if (frToken) fd.append('fr_token', frToken);
180
184
  fd.append('fr_image', frImageFiles[0]);
181
185
  fd.append('fr_image2', frImageFiles[1] || frImageFiles[0]);
182
186
  return request('POST', '/otp/verify', fd);
@@ -112,9 +112,10 @@ export class LoginFlowController {
112
112
  }
113
113
 
114
114
  /** After social OAuth callback: we're in social flow, optionally at FRS */
115
- setSocialCallback(provider) {
115
+ setSocialCallback(provider, { twoFactorRequired = false } = {}) {
116
116
  this.currentMethod = provider === 'microsoft' ? LOGIN_METHODS.SOCIAL_MICROSOFT : LOGIN_METHODS.SOCIAL_GOOGLE;
117
- this.steps = getStepsForMethod(this.currentMethod, this.config.frsAsSecondFactor);
117
+ const useFrs = twoFactorRequired || this.config.frsAsSecondFactor;
118
+ this.steps = getStepsForMethod(this.currentMethod, useFrs);
118
119
  this.stepIndex = 0;
119
120
  this._notify();
120
121
  }
@@ -12,6 +12,9 @@ export class LoginStateManager {
12
12
  faceCapture: null,
13
13
  loginToken: null,
14
14
  socialProvider: null,
15
+ frToken: null,
16
+ frTokenExpiresInS: null,
17
+ twoFactorMessage: '',
15
18
  error: '',
16
19
  };
17
20
  this.listeners = new Set();
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Shared OAuth redirect handling for login and signup flows.
3
+ * Backend 302-redirects to redirect_uri with query params after Google/Microsoft auth.
4
+ */
5
+
6
+ export const OAUTH_PARAM_KEYS = [
7
+ 'success', 'provider', 'token', 'login_token', 'two_factor_required', 'fr_token',
8
+ 'fr_token_expires_in_s', 'next_step', 'message', 'user_id', 'email', 'entity_domain', 'error',
9
+ ];
10
+
11
+ export const OAUTH_PENDING_CLASS = 'central-auth-oauth-pending';
12
+
13
+ /** @returns {boolean} True when the URL looks like an OAuth redirect callback */
14
+ export function hasOAuthRedirect() {
15
+ if (typeof window === 'undefined' || !window.location) return false;
16
+ const params = new URLSearchParams(window.location.search);
17
+ return params.has('provider')
18
+ || params.has('login_token')
19
+ || params.has('token')
20
+ || params.has('success');
21
+ }
22
+
23
+ /**
24
+ * Parse OAuth redirect query params.
25
+ * @returns {Object|null} Parsed redirect payload
26
+ */
27
+ export function parseOAuthRedirect() {
28
+ if (!hasOAuthRedirect()) return null;
29
+
30
+ const params = new URLSearchParams(window.location.search);
31
+ const provider = (params.get('provider') || '').toLowerCase();
32
+ const hasProvider = provider === 'google' || provider === 'microsoft';
33
+ const loginToken = params.get('login_token');
34
+ const token = params.get('token');
35
+ const successParam = params.get('success');
36
+ const error = params.get('error');
37
+ const nextStep = (params.get('next_step') || '').toUpperCase();
38
+
39
+ const shared = {
40
+ provider: hasProvider ? provider : 'google',
41
+ email: params.get('email'),
42
+ userId: params.get('user_id'),
43
+ entityDomain: params.get('entity_domain'),
44
+ message: params.get('message'),
45
+ nextStep,
46
+ };
47
+
48
+ if (successParam === 'false' || error) {
49
+ return { ...shared, kind: 'error', error: error || 'Login failed' };
50
+ }
51
+
52
+ const twoFactorRaw = params.get('two_factor_required');
53
+ const twoFactorRequired = twoFactorRaw === 'true' || twoFactorRaw === '1' || nextStep === 'FR_VERIFICATION';
54
+
55
+ if (loginToken) {
56
+ return {
57
+ ...shared,
58
+ kind: 'pending',
59
+ loginToken,
60
+ twoFactorRequired,
61
+ frToken: params.get('fr_token') || null,
62
+ frTokenExpiresInS: params.get('fr_token_expires_in_s') || null,
63
+ };
64
+ }
65
+
66
+ if (token) {
67
+ return { ...shared, kind: 'complete', token };
68
+ }
69
+
70
+ return null;
71
+ }
72
+
73
+ /** @param {ReturnType<parseOAuthRedirect>} redirect */
74
+ export function getOAuthRedirectFlowType(redirect) {
75
+ if (!redirect) return null;
76
+ if (redirect.nextStep === 'FR_ENROLLMENT' || redirect.nextStep === 'MOBILE_VERIFICATION') {
77
+ return 'signup';
78
+ }
79
+ return 'login';
80
+ }
81
+
82
+ /** Remove OAuth params from the URL without reloading */
83
+ export function clearOAuthRedirectParams() {
84
+ if (typeof window === 'undefined' || !window.history) return;
85
+ const params = new URLSearchParams(window.location.search);
86
+ OAUTH_PARAM_KEYS.forEach((key) => params.delete(key));
87
+ const clean = params.toString();
88
+ const url = window.location.pathname + (clean ? '?' + clean : '') + window.location.hash;
89
+ window.history.replaceState({}, '', url);
90
+ }
91
+
92
+ /**
93
+ * Mark the document while OAuth resume is pending (reduces parent-app flash).
94
+ * Parent apps can hide main UI with:
95
+ * html.central-auth-oauth-pending [data-central-auth-main] { display: none !important; }
96
+ * html.central-auth-oauth-pending [data-central-auth-sdk] { display: block !important; }
97
+ */
98
+ export function markOAuthRedirectPending() {
99
+ if (!hasOAuthRedirect() || typeof document === 'undefined') return;
100
+ document.documentElement.classList.add(OAUTH_PENDING_CLASS);
101
+ }
102
+
103
+ export function clearOAuthRedirectPendingMark() {
104
+ if (typeof document === 'undefined') return;
105
+ document.documentElement.classList.remove(OAUTH_PENDING_CLASS);
106
+ }
package/index.js CHANGED
@@ -4,17 +4,31 @@
4
4
  * APIs wired to Central Auth (central_auth_collection.json)
5
5
  *
6
6
  * Public API for host applications:
7
- * initializeAuth(config) — set default configuration
7
+ * initializeAuth(config) — set default configuration (+ auto-resume OAuth when configured)
8
8
  * startSignup(config) — run sign-up flow in a container
9
- * startLogin(config) — run login flow in a container
9
+ * startLogin(config) — run login flow in a container
10
+ * autoResumeOAuthFlow(config) — mount login/signup after OAuth redirect (no-op otherwise)
10
11
  *
11
12
  * Lower-level: mount (signup), mountLogin (login)
12
13
  */
13
14
 
15
+ import { markOAuthRedirectPending } from './core/oauth-redirect.js';
16
+
17
+ markOAuthRedirectPending();
18
+
14
19
  export { mount } from './signup.js';
15
20
  export { FlowController, STEPS } from './core/flow.js';
16
21
  export { StateManager } from './core/state.js';
17
22
  export { createApi, dataUrlToFile, base64ToFile } from './core/api.js';
18
23
  export { mountLogin } from './login.js';
19
- export { initializeAuth, startSignup, startLogin } from './auth.js';
24
+ export { initializeAuth, startSignup, startLogin, autoResumeOAuthFlow } from './auth.js';
20
25
  export { applyTheme, THEME_PRESETS } from './theme.js';
26
+ export {
27
+ hasOAuthRedirect,
28
+ hasOAuthRedirect as hasPendingOAuthRedirect,
29
+ parseOAuthRedirect,
30
+ getOAuthRedirectFlowType,
31
+ markOAuthRedirectPending,
32
+ clearOAuthRedirectPendingMark,
33
+ OAUTH_PENDING_CLASS,
34
+ } from './core/oauth-redirect.js';
package/login.js CHANGED
@@ -8,29 +8,24 @@ import { resolveApiBaseUrl } from './core/env-config.js';
8
8
  import { normalizeLoginConfig, getEnabledLoginMethods, LOGIN_METHODS } from './core/login-config.js';
9
9
  import { LoginFlowController, LOGIN_STEPS } from './core/login-flow.js';
10
10
  import { LoginStateManager } from './core/login-state.js';
11
+ import { parseOAuthRedirect, clearOAuthRedirectParams, clearOAuthRedirectPendingMark } from './core/oauth-redirect.js';
11
12
  import { createLayout } from './ui/layout.js';
12
13
  import { createLoginScreen } from './ui/screens/login/index.js';
13
14
  import { applyTheme } from './theme.js';
14
15
 
15
- /** Parse OAuth callback (login_token, provider) from URL */
16
- function parseOAuthCallback() {
17
- if (typeof window === 'undefined' || !window.location) return null;
18
- const params = new URLSearchParams(window.location.search);
19
- const loginToken = params.get('login_token');
20
- const provider = (params.get('provider') || '').toLowerCase();
21
- if (!loginToken) return null;
22
- if (provider !== 'google' && provider !== 'microsoft') return { loginToken, provider: 'google' };
23
- return { loginToken, provider };
16
+ function isTwoFactorRequired(res) {
17
+ if (!res || typeof res !== 'object') return false;
18
+ if (res.two_factor_required === true) return true;
19
+ const step = String(res.next_step || '').toUpperCase();
20
+ return step === 'FR_VERIFICATION';
24
21
  }
25
22
 
26
- function clearOAuthParams() {
27
- if (typeof window === 'undefined' || !window.history) return;
28
- const params = new URLSearchParams(window.location.search);
29
- params.delete('login_token');
30
- params.delete('provider');
31
- const clean = params.toString();
32
- const url = window.location.pathname + (clean ? '?' + clean : '') + window.location.hash;
33
- window.history.replaceState({}, '', url);
23
+ function applyTwoFactorFromResponse(res, state) {
24
+ if (res.login_token) state.set('loginToken', res.login_token);
25
+ if (res.fr_token) state.set('frToken', res.fr_token);
26
+ if (res.fr_token_expires_in_s != null) state.set('frTokenExpiresInS', res.fr_token_expires_in_s);
27
+ if (res.provider) state.set('socialProvider', String(res.provider).toLowerCase());
28
+ if (res.message) state.set('twoFactorMessage', res.message);
34
29
  }
35
30
 
36
31
  /**
@@ -65,12 +60,36 @@ export function mountLogin(container, options = {}) {
65
60
  const cleanupTheme = applyTheme(layout.root, theme);
66
61
  const showError = (msg) => layout.showError(msg);
67
62
 
68
- const oauth = parseOAuthCallback();
63
+ const oauth = parseOAuthRedirect();
69
64
  if (oauth) {
70
- state.set('loginToken', oauth.loginToken);
71
- state.set('socialProvider', oauth.provider);
72
- clearOAuthParams();
73
- flow.setSocialCallback(oauth.provider);
65
+ clearOAuthRedirectParams();
66
+ if (oauth.kind === 'error') {
67
+ state.set('error', oauth.error);
68
+ } else if (oauth.kind === 'complete') {
69
+ if (oauth.email) state.set('identifier', oauth.email);
70
+ const result = {
71
+ token: oauth.token,
72
+ user_id: oauth.userId,
73
+ email: oauth.email,
74
+ entity_domain: oauth.entityDomain,
75
+ message: oauth.message,
76
+ provider: oauth.provider,
77
+ };
78
+ state.set('loginResult', result);
79
+ flow.setSocialCallback(oauth.provider, { twoFactorRequired: false });
80
+ flow.setStep(LOGIN_STEPS.SUCCESS);
81
+ if (typeof onLoginComplete === 'function') {
82
+ onLoginComplete(buildLoginCompletePayload(result));
83
+ }
84
+ } else if (oauth.kind === 'pending') {
85
+ state.set('loginToken', oauth.loginToken);
86
+ state.set('socialProvider', oauth.provider);
87
+ if (oauth.email) state.set('identifier', oauth.email);
88
+ if (oauth.frToken) state.set('frToken', oauth.frToken);
89
+ if (oauth.frTokenExpiresInS) state.set('frTokenExpiresInS', oauth.frTokenExpiresInS);
90
+ if (oauth.message) state.set('twoFactorMessage', oauth.message);
91
+ flow.setSocialCallback(oauth.provider, { twoFactorRequired: oauth.twoFactorRequired });
92
+ }
74
93
  }
75
94
 
76
95
  const enabled = getEnabledLoginMethods(config);
@@ -104,6 +123,19 @@ export function mountLogin(container, options = {}) {
104
123
  return payload;
105
124
  }
106
125
 
126
+ function completeLogin(apiResult) {
127
+ state.set('loginResult', apiResult);
128
+ flow.goNext();
129
+ if (typeof onLoginComplete === 'function') {
130
+ onLoginComplete(buildLoginCompletePayload(apiResult));
131
+ }
132
+ }
133
+
134
+ function requireFaceVerification(res) {
135
+ applyTwoFactorFromResponse(res, state);
136
+ flow.injectFrsStep();
137
+ }
138
+
107
139
  async function handleIdentifierEmail(email) {
108
140
  state.clearError();
109
141
  state.set('identifier', email);
@@ -153,11 +185,11 @@ export function mountLogin(container, options = {}) {
153
185
  }
154
186
  try {
155
187
  const res = await api.loginCustom(identifier, password, config.appIdentifier, null);
156
- state.set('loginResult', res);
157
- flow.goNext();
158
- if (typeof onLoginComplete === 'function') {
159
- onLoginComplete(buildLoginCompletePayload(res));
188
+ if (isTwoFactorRequired(res)) {
189
+ requireFaceVerification(res);
190
+ return;
160
191
  }
192
+ completeLogin(res);
161
193
  } catch (err) {
162
194
  if (isFrRequiredError(err)) {
163
195
  flow.injectFrsStep();
@@ -180,6 +212,10 @@ export function mountLogin(container, options = {}) {
180
212
  }
181
213
  try {
182
214
  const res = await api.otpVerify(identifier, '', channel, otp);
215
+ if (isTwoFactorRequired(res)) {
216
+ requireFaceVerification(res);
217
+ return;
218
+ }
183
219
  state.set('loginResult', res);
184
220
  flow.setStep(LOGIN_STEPS.SUCCESS);
185
221
  if (typeof onLoginComplete === 'function') {
@@ -206,6 +242,7 @@ export function mountLogin(container, options = {}) {
206
242
  }
207
243
  const method = flow.currentMethod;
208
244
  const identifier = state.get('identifier');
245
+ const frToken = state.get('frToken');
209
246
  let apiResult = null;
210
247
 
211
248
  layout.setLoading(true, 'Verifying your face…');
@@ -213,20 +250,16 @@ export function mountLogin(container, options = {}) {
213
250
  if (method === LOGIN_METHODS.EMAIL_FRS) {
214
251
  apiResult = await api.loginEmailFr(identifier, file, null, config.appIdentifier);
215
252
  } else if (method === LOGIN_METHODS.EMAIL_PASSWORD || method === LOGIN_METHODS.MOBILE_PASSWORD) {
216
- apiResult = await api.loginCustom(identifier, state.get('password'), config.appIdentifier, [file, file]);
253
+ apiResult = await api.loginCustom(identifier, state.get('password'), config.appIdentifier, [file, file], frToken);
217
254
  } else if (method === LOGIN_METHODS.SOCIAL_GOOGLE) {
218
- apiResult = await api.googleFrEnroll(state.get('loginToken'), file, null, config.appIdentifier);
255
+ apiResult = await api.googleFrEnroll(state.get('loginToken'), file, null, config.appIdentifier, frToken);
219
256
  } else if (method === LOGIN_METHODS.SOCIAL_MICROSOFT) {
220
- apiResult = await api.microsoftFrEnroll(state.get('loginToken'), file, null, config.appIdentifier);
257
+ apiResult = await api.microsoftFrEnroll(state.get('loginToken'), file, null, config.appIdentifier, frToken);
221
258
  } else if (method === LOGIN_METHODS.EMAIL_OTP || method === LOGIN_METHODS.MOBILE_OTP) {
222
259
  const channel = identifier.includes('@') ? 'email' : 'sms';
223
- apiResult = await api.otpVerify(identifier, '', channel, state.get('otp'), [file, file]);
224
- }
225
- state.set('loginResult', apiResult);
226
- flow.goNext();
227
- if (typeof onLoginComplete === 'function') {
228
- onLoginComplete(buildLoginCompletePayload(apiResult));
260
+ apiResult = await api.otpVerify(identifier, '', channel, state.get('otp'), [file, file], frToken);
229
261
  }
262
+ completeLogin(apiResult);
230
263
  } catch (err) {
231
264
  const msg = err?.data?.error || err?.message || 'Verification failed';
232
265
  state.set('error', msg);
@@ -312,6 +345,12 @@ export function mountLogin(container, options = {}) {
312
345
 
313
346
  flow.subscribe(render);
314
347
  render();
348
+ if (oauth?.kind === 'error') {
349
+ showError(oauth.error);
350
+ }
351
+ if (oauth) {
352
+ clearOAuthRedirectPendingMark();
353
+ }
315
354
 
316
355
  return {
317
356
  unmount() { cleanupTheme(); layout.root.remove(); },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "central-auth-js-sdk",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
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
@@ -7,30 +7,21 @@ import { FlowController, STEPS } from './core/flow.js';
7
7
  import { StateManager } from './core/state.js';
8
8
  import { createApi, dataUrlToFile } from './core/api.js';
9
9
  import { resolveApiBaseUrl } from './core/env-config.js';
10
+ import { parseOAuthRedirect, clearOAuthRedirectParams } from './core/oauth-redirect.js';
10
11
  import { createLayout } from './ui/layout.js';
11
12
  import { createScreen } from './ui/screens/index.js';
12
13
  import { applyTheme } from './theme.js';
13
14
 
14
15
  /** Parse URL query for OAuth callback params (login_token, provider) */
15
- function parseOAuthCallback() {
16
- if (typeof window === 'undefined' || !window.location) return null;
17
- const params = new URLSearchParams(window.location.search);
18
- const loginToken = params.get('login_token');
19
- const provider = (params.get('provider') || '').toLowerCase();
20
- if (!loginToken) return null;
21
- if (provider !== 'google' && provider !== 'microsoft') return { loginToken, provider: 'google' };
22
- return { loginToken, provider };
16
+ function parseSignupOAuthCallback() {
17
+ const redirect = parseOAuthRedirect();
18
+ if (!redirect || !redirect.loginToken) return null;
19
+ return redirect;
23
20
  }
24
21
 
25
22
  /** Remove OAuth params from URL without reload */
26
23
  function clearOAuthParams() {
27
- if (typeof window === 'undefined' || !window.history) return;
28
- const params = new URLSearchParams(window.location.search);
29
- params.delete('login_token');
30
- params.delete('provider');
31
- const clean = params.toString();
32
- const url = window.location.pathname + (clean ? '?' + clean : '') + window.location.hash;
33
- window.history.replaceState({}, '', url);
24
+ clearOAuthRedirectParams();
34
25
  }
35
26
 
36
27
  /** Map API next_step from POST /signup/start to our flow step */
@@ -90,12 +81,18 @@ export function mount(container, options = {}) {
90
81
  const showError = (msg) => layout.showError(msg);
91
82
 
92
83
  // SDK handles OAuth callback: if URL has login_token, resume social flow
93
- const oauth = parseOAuthCallback();
84
+ const oauth = parseSignupOAuthCallback();
94
85
  if (oauth) {
95
86
  state.set('loginToken', oauth.loginToken);
96
87
  state.set('socialProvider', oauth.provider);
88
+ if (oauth.email) state.set('email', oauth.email);
97
89
  clearOAuthParams();
98
90
  flow.setSocialFlow();
91
+ if (oauth.nextStep === 'FR_ENROLLMENT') {
92
+ flow.setStep(STEPS.FACE);
93
+ } else if (oauth.nextStep === 'MOBILE_VERIFICATION') {
94
+ flow.setStep(STEPS.MOBILE);
95
+ }
99
96
  }
100
97
 
101
98
  const isSocial = () => !!state.get('loginToken');