@stack-spot/auth-react 2.7.1 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.8.0](https://github.com/stack-spot/portal-auth-js/compare/auth-react@v2.7.1...auth-react@v2.8.0) (2025-04-28)
4
+
5
+
6
+ ### Features
7
+
8
+ * two step login idp sso ([#98](https://github.com/stack-spot/portal-auth-js/issues/98)) ([1c6bd24](https://github.com/stack-spot/portal-auth-js/commit/1c6bd2429fd17bf869f834b8d813a1b5c4f332a2))
9
+
3
10
  ## [2.7.1](https://github.com/stack-spot/portal-auth-js/compare/auth-react@v2.7.0...auth-react@v2.7.1) (2025-04-25)
4
11
 
5
12
 
package/out/index.d.ts CHANGED
@@ -1,31 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { Session, ThirdPartyLoginParams, AuthConfig, ThirdPartyAuthType } from '@stack-spot/auth';
3
3
 
4
- type LoginType = 'sso' | 'idp';
5
- interface BaseData {
6
- type: LoginType;
7
- }
8
- interface SSOData extends BaseData {
9
- type: 'sso';
10
- email: string;
11
- }
12
- interface IDPData extends BaseData {
13
- type: 'idp';
14
- provider: 'external-idp:github' | 'external-idp:google' | 'external-idp:microsoft';
15
- }
16
- type LoginData = SSOData | IDPData;
17
- type LoginProps = {
18
- initialValue?: string;
19
- onSubmit: (data: LoginData) => Promise<void>;
20
- welcomeText?: string;
21
- removeLoadingOnSuccess?: boolean;
22
- className?: string;
23
- style?: React.CSSProperties;
24
- banner?: React.ReactNode;
25
- loginTypes?: LoginType[];
26
- };
27
- declare const Login: ({ onSubmit, initialValue, welcomeText, removeLoadingOnSuccess, className, style, banner, loginTypes }: LoginProps) => react_jsx_runtime.JSX.Element;
28
-
29
4
  interface SessionManagerConfig extends Pick<AuthConfig, 'accountUrl' | 'authUrl' | 'clientId' | 'defaultTenant' | 'retry' | 'retryDelay'> {
30
5
  /**
31
6
  * The URL to redirect to when the user logs out.
@@ -84,6 +59,30 @@ declare class SessionManager {
84
59
  getTrialEnabledProviders(): Promise<string[]>;
85
60
  }
86
61
 
62
+ type LoginType = 'sso' | 'idp';
63
+ interface BaseData {
64
+ type: LoginType;
65
+ }
66
+ interface SSOData extends BaseData {
67
+ type: 'sso';
68
+ email: string;
69
+ }
70
+ interface IDPData extends BaseData {
71
+ type: 'idp';
72
+ provider: 'external-idp:github' | 'external-idp:google' | 'external-idp:microsoft';
73
+ }
74
+ type LoginData = SSOData | IDPData;
75
+ type LoginProps = {
76
+ initialValue?: string;
77
+ onSubmit: (data: LoginData) => Promise<void>;
78
+ welcomeText?: string;
79
+ removeLoadingOnSuccess?: boolean;
80
+ className?: string;
81
+ style?: React.CSSProperties;
82
+ banner?: React.ReactNode;
83
+ loginTypes?: LoginType[];
84
+ };
85
+
87
86
  type AuthStatus = 'unknown' | 'authenticated' | 'unauthenticated';
88
87
  interface Props {
89
88
  children: React.ReactElement;
@@ -97,4 +96,6 @@ declare const Authenticated: ({ children, onLogin, onSession, customLoginProps,
97
96
 
98
97
  declare function useSession(): Session | undefined;
99
98
 
99
+ declare const Login: ({ onSubmit, initialValue, welcomeText, removeLoadingOnSuccess, className, style, banner, loginTypes }: LoginProps) => react_jsx_runtime.JSX.Element;
100
+
100
101
  export { Authenticated, Login, SessionManager, useSession };
package/out/index.js CHANGED
@@ -8,9 +8,49 @@ require('@stack-spot/portal-theme/dist/theme.css');
8
8
  var portalTranslate = require('@stack-spot/portal-translate');
9
9
  var react = require('react');
10
10
  var ui = require('@citric/ui');
11
+ var auth = require('@stack-spot/auth');
11
12
  var svg = require('@stack-spot/portal-components/svg');
12
13
  var styledComponents = require('styled-components');
13
- var auth = require('@stack-spot/auth');
14
+ var lodash = require('lodash');
15
+ require('@citric/icons');
16
+
17
+ const dictionary = {
18
+ en: {
19
+ welcome: "Welcome to StackSpot",
20
+ loginWithEmail: "Log in with your email.",
21
+ loginWithSocialAccount: "Sign up or access your free trial with a social account",
22
+ label: "Corporate email",
23
+ placeholder: "email@company.com",
24
+ continue: "Continue",
25
+ or: "Or",
26
+ loginWith: "Sign in with $0",
27
+ emailNotAllowedTitle: "Your email is linked to an Enterprise account.",
28
+ emailNotAllowedSubtitle: "Please log in with your corporate email.",
29
+ socialLogin: "Login or register with a social account",
30
+ corporateLoginTitle: "Already have a StackSpot enterprise account?",
31
+ corporateLoginButton: "Login with enterprise account",
32
+ socialLoginTitle: "Do you want to access another way?",
33
+ emailNotFoundError: "We couldn't find an account for this email"
34
+ },
35
+ pt: {
36
+ welcome: "Boas vindas \xE0 StackSpot",
37
+ loginWithEmail: "Fa\xE7a login com seu e-mail.",
38
+ loginWithSocialAccount: "Cadastre-se ou acesse seu teste gratuito com uma conta social",
39
+ label: "Email corporativo",
40
+ placeholder: "email@empresa.com",
41
+ continue: "Continuar",
42
+ or: "Ou",
43
+ loginWith: "Entrar com $0",
44
+ emailNotAllowedTitle: '"Este e-mail est\xE1 vinculado a uma conta Enterprise.',
45
+ emailNotAllowedSubtitle: "Fa\xE7a login com seu email corporativo.",
46
+ socialLogin: "Entre ou cadastre-se com uma conta social",
47
+ corporateLoginTitle: "J\xE1 possui uma conta StackSpot Enterprise?",
48
+ corporateLoginButton: "Entrar na conta Enterprise",
49
+ socialLoginTitle: "Voc\xEA quer entrar de outro jeito?",
50
+ emailNotFoundError: "N\xE3o encontramos uma conta para este e-mail."
51
+ }
52
+ };
53
+ const useTranslation = () => portalTranslate.useTranslate(dictionary);
14
54
 
15
55
  const sessionKey$1 = `stk-session${portalComponents.getCookieDomain()}`;
16
56
  const sessionCookie = Object.freeze({
@@ -291,9 +331,14 @@ const useTrialProviders = ({ enabled = true }) => {
291
331
  (async () => {
292
332
  if (!SessionManager.instance || !enabled)
293
333
  return;
294
- const providers = await SessionManager.instance.getTrialEnabledProviders();
295
- setTrialProviders(providers);
296
- setIsLoadingTrialProviders(false);
334
+ try {
335
+ const providers = await SessionManager.instance.getTrialEnabledProviders();
336
+ setTrialProviders(providers);
337
+ setIsLoadingTrialProviders(false);
338
+ } catch (error) {
339
+ console.error(error);
340
+ setIsLoadingTrialProviders(false);
341
+ }
297
342
  })();
298
343
  }, [SessionManager.instance]);
299
344
  return [trialProviders, isLoadingTrialProviders];
@@ -387,6 +432,73 @@ const Microsoft = react.forwardRef((props, ref) => /* @__PURE__ */ jsxRuntime.js
387
432
  /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12.8477 12.3994H19.8042V19.3559H12.8477V12.3994Z", fill: "#FFBA08" })
388
433
  ] })));
389
434
 
435
+ const providerIcons = {
436
+ github: /* @__PURE__ */ jsxRuntime.jsx(Github, {}),
437
+ google: /* @__PURE__ */ jsxRuntime.jsx(Google, {}),
438
+ microsoft: /* @__PURE__ */ jsxRuntime.jsx(Microsoft, {})
439
+ };
440
+ const ButtonProvider = ({ provider, login, loading, disabled }) => {
441
+ const t = useTranslation();
442
+ return /* @__PURE__ */ jsxRuntime.jsx(core.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(
443
+ core.Button,
444
+ {
445
+ colorScheme: "light",
446
+ type: "button",
447
+ size: "md",
448
+ sx: { width: "100%" },
449
+ onClick: () => login("idp", provider),
450
+ disabled: loading || disabled,
451
+ children: loading ? /* @__PURE__ */ jsxRuntime.jsx(ui.LoadingCircular, {}) : /* @__PURE__ */ jsxRuntime.jsxs(core.Flex, { alignItems: "center", style: { gap: "4px" }, children: [
452
+ providerIcons[provider],
453
+ portalTranslate.interpolate(t.loginWith, lodash.capitalize(provider))
454
+ ] })
455
+ }
456
+ ) });
457
+ };
458
+ const IDPLogin = ({ trialProviders, loading, loginProvider, onSubmit, onChangeMode }) => {
459
+ const t = useTranslation();
460
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
461
+ /* @__PURE__ */ jsxRuntime.jsx(core.Flex, { flexDirection: "column", gap: true, children: trialProviders == null ? void 0 : trialProviders.map((provider) => /* @__PURE__ */ jsxRuntime.jsx(
462
+ ButtonProvider,
463
+ {
464
+ provider,
465
+ login: onSubmit,
466
+ loading: loading && loginProvider === provider,
467
+ disabled: loading
468
+ },
469
+ provider
470
+ )) }),
471
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "separator", children: /* @__PURE__ */ jsxRuntime.jsx(core.Text, { appearance: "microtext1", colorScheme: "light.700", children: t.or }) }),
472
+ /* @__PURE__ */ jsxRuntime.jsx(core.Text, { colorScheme: "light.700", align: "center", children: t.corporateLoginTitle }),
473
+ /* @__PURE__ */ jsxRuntime.jsx(core.Button, { size: "md", disabled: loading, colorScheme: "light", onClick: () => onChangeMode("sso"), children: t.corporateLoginButton })
474
+ ] });
475
+ };
476
+
477
+ const lastLoginTypeKey = "lastLoginType";
478
+ function getLastLoginType() {
479
+ const type = localStorage.getItem(lastLoginTypeKey);
480
+ if (type === "idp" || type === "sso")
481
+ return type;
482
+ return localStorage.getItem("guided-tour") ? "sso" : "idp";
483
+ }
484
+ function setLastLoginType(type) {
485
+ localStorage.setItem(lastLoginTypeKey, type);
486
+ }
487
+
488
+ const SSOLogin = ({ value, onChange, loading, disabled, hasProvider, onChangeMode }) => {
489
+ const t = useTranslation();
490
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
491
+ /* @__PURE__ */ jsxRuntime.jsxs(core.Flex, { flexDirection: "column", style: { gap: "4px", marginTop: "4px" }, children: [
492
+ /* @__PURE__ */ jsxRuntime.jsx(core.Label, { htmlFor: "email", children: t.label }),
493
+ /* @__PURE__ */ jsxRuntime.jsx(core.Input, { id: "email", type: "email", name: "email", value, onChange: (e) => onChange(e.target.value), placeholder: t.placeholder }),
494
+ /* @__PURE__ */ jsxRuntime.jsx(core.Button, { colorScheme: "primary", size: "md", style: { marginTop: "12px" }, disabled: disabled || loading, children: loading && !hasProvider ? /* @__PURE__ */ jsxRuntime.jsx(ui.LoadingCircular, {}) : /* @__PURE__ */ jsxRuntime.jsx(core.Text, { children: t.continue }) })
495
+ ] }),
496
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "separator", children: /* @__PURE__ */ jsxRuntime.jsx(core.Text, { appearance: "microtext1", colorScheme: "light.700", children: t.or }) }),
497
+ /* @__PURE__ */ jsxRuntime.jsx(core.Text, { colorScheme: "light.700", align: "center", children: t.socialLoginTitle }),
498
+ /* @__PURE__ */ jsxRuntime.jsx(core.Button, { size: "md", disabled: loading, colorScheme: "light", onClick: () => onChangeMode("idp"), children: t.socialLogin })
499
+ ] });
500
+ };
501
+
390
502
  const LoginBox = styledComponents.styled.form`
391
503
  display: flex;
392
504
  flex-direction: column;
@@ -422,39 +534,15 @@ const LoginBox = styledComponents.styled.form`
422
534
  line-height: 1.5rem;
423
535
  }
424
536
  `;
425
- const providerIcons = {
426
- github: /* @__PURE__ */ jsxRuntime.jsx(Github, {}),
427
- google: /* @__PURE__ */ jsxRuntime.jsx(Google, {}),
428
- microsoft: /* @__PURE__ */ jsxRuntime.jsx(Microsoft, {})
429
- };
430
- function capitalize(str) {
431
- return str.charAt(0).toUpperCase() + str.slice(1);
432
- }
433
- const ButtonProvider = ({ provider, login, loading }) => {
434
- const t = portalTranslate.useTranslate(dictionary);
435
- return /* @__PURE__ */ jsxRuntime.jsx(core.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(core.Button, { colorScheme: "light", type: "button", size: "md", sx: { width: "100%" }, onClick: () => login("idp", provider), disabled: loading, children: loading ? /* @__PURE__ */ jsxRuntime.jsx(ui.LoadingCircular, {}) : /* @__PURE__ */ jsxRuntime.jsxs(core.Flex, { alignItems: "center", style: { gap: "4px" }, children: [
436
- providerIcons[provider],
437
- portalTranslate.interpolate(t.loginWith, capitalize(provider))
438
- ] }) }) });
439
- };
440
537
  const EmailNotAllowed = () => {
441
- const t = portalTranslate.useTranslate(dictionary);
538
+ const t = useTranslation();
442
539
  return /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { children: /* @__PURE__ */ jsxRuntime.jsxs(core.Flex, { justifyContent: "center", children: [
443
540
  /* @__PURE__ */ jsxRuntime.jsx(core.Text, { appearance: "body2", children: t.emailNotAllowedTitle }),
444
541
  /* @__PURE__ */ jsxRuntime.jsx(core.Text, { appearance: "body2", colorScheme: "light.700", children: t.emailNotAllowedSubtitle })
445
542
  ] }) });
446
543
  };
447
- const Login = ({
448
- onSubmit,
449
- initialValue = "",
450
- welcomeText,
451
- removeLoadingOnSuccess,
452
- className,
453
- style,
454
- banner,
455
- loginTypes = ["idp", "sso"]
456
- }) => {
457
- const t = portalTranslate.useTranslate(dictionary);
544
+ const Login = ({ onSubmit, initialValue = "", welcomeText, removeLoadingOnSuccess, className, style, banner, loginTypes = ["idp", "sso"] }) => {
545
+ const t = useTranslation();
458
546
  const [trialProviders, isLoadingTrialProviders] = useTrialProviders({ enabled: loginTypes.includes("idp") });
459
547
  const searchParams = new URLSearchParams(location.search);
460
548
  const [error, setError] = react.useState(searchParams.get("error_description") || searchParams.get("error") || "");
@@ -465,7 +553,10 @@ const Login = ({
465
553
  const [email, setEmail] = react.useState(initialValue || searchParams.get("email") || "");
466
554
  const disabled = !email.match(/\w+@\w+/);
467
555
  const idpLoginEnabled = loginTypes.includes("idp") && !!(trialProviders == null ? void 0 : trialProviders.length);
468
- const ssoLoginEnabled = loginTypes.includes("sso");
556
+ const [mode, setMode] = react.useState();
557
+ react.useEffect(() => {
558
+ setMode(idpLoginEnabled ? getLastLoginType() : "sso");
559
+ }, [idpLoginEnabled]);
469
560
  react.useEffect(() => {
470
561
  if (!providerQueryParam)
471
562
  return;
@@ -480,13 +571,18 @@ const Login = ({
480
571
  provider !== loginProvider && setLoginProvider(provider);
481
572
  try {
482
573
  const data = type === "idp" && !!provider ? { type: "idp", provider: `external-idp:${provider}` } : { type: "sso", email };
574
+ setLastLoginType(data.type);
483
575
  await onSubmit(data);
484
576
  if (removeLoadingOnSuccess)
485
577
  setLoading(false);
486
578
  } catch (error2) {
487
579
  setLoading(false);
488
580
  setLoginProvider(void 0);
489
- setError(error2.message || error2.toString());
581
+ if (error2 instanceof auth.AuthMethodUnavailable) {
582
+ setError(t.emailNotFoundError);
583
+ } else {
584
+ setError(error2.message || error2.toString());
585
+ }
490
586
  }
491
587
  }
492
588
  function submitForm(e) {
@@ -495,80 +591,47 @@ const Login = ({
495
591
  return;
496
592
  login("sso");
497
593
  }
594
+ if (isLoadingTrialProviders || !mode) {
595
+ return /* @__PURE__ */ jsxRuntime.jsxs(core.Flex, { alignContent: "center", justifyContent: "center", my: 5, children: [
596
+ /* @__PURE__ */ jsxRuntime.jsx(ui.LoadingCircular, {}),
597
+ " "
598
+ ] });
599
+ }
498
600
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
499
601
  /* @__PURE__ */ jsxRuntime.jsxs(LoginBox, { onSubmit: submitForm, className, style, children: [
500
602
  /* @__PURE__ */ jsxRuntime.jsxs("header", { children: [
501
603
  /* @__PURE__ */ jsxRuntime.jsx(svg.MiniLogo, {}),
502
604
  /* @__PURE__ */ jsxRuntime.jsxs(core.Flex, { flexDirection: "column", alignItems: "center", children: [
503
605
  /* @__PURE__ */ jsxRuntime.jsx(core.Text, { appearance: "body1", weight: "medium", children: welcomeText || t.welcome }),
504
- /* @__PURE__ */ jsxRuntime.jsxs(core.Text, { appearance: "body2", colorScheme: "light.700", children: [
505
- " ",
506
- t.loginWithEmail
507
- ] })
606
+ /* @__PURE__ */ jsxRuntime.jsx(core.Text, { appearance: "body2", colorScheme: "light.700", children: mode === "idp" ? t.loginWithSocialAccount : t.loginWithEmail })
508
607
  ] })
509
608
  ] }),
510
609
  errorCode && errorCode === "EMAIL_IS_NOT_ALLOWED" && /* @__PURE__ */ jsxRuntime.jsx(EmailNotAllowed, {}),
511
- ssoLoginEnabled && /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsxs(core.Flex, { flexDirection: "column", style: { gap: "4px", marginTop: "4px" }, children: [
512
- /* @__PURE__ */ jsxRuntime.jsx(core.Label, { htmlFor: "email", children: t.label }),
513
- /* @__PURE__ */ jsxRuntime.jsx(core.Input, { type: "email", name: "email", value: email, onChange: (e) => setEmail(e.target.value), placeholder: t.placeholder }),
514
- /* @__PURE__ */ jsxRuntime.jsx(core.Button, { colorScheme: "primary", size: "md", style: { marginTop: "12px" }, disabled: disabled || loading, children: loading && !loginProvider ? /* @__PURE__ */ jsxRuntime.jsx(ui.LoadingCircular, {}) : /* @__PURE__ */ jsxRuntime.jsx(core.Text, { children: t.continue }) })
515
- ] }) }),
516
- isLoadingTrialProviders ? /* @__PURE__ */ jsxRuntime.jsxs(core.Flex, { alignContent: "center", justifyContent: "center", my: 5, children: [
517
- " ",
518
- /* @__PURE__ */ jsxRuntime.jsx(ui.LoadingCircular, {}),
519
- " "
520
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
521
- ssoLoginEnabled && idpLoginEnabled && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "separator", children: /* @__PURE__ */ jsxRuntime.jsx(core.Text, { appearance: "microtext1", colorScheme: "light.700", children: t.or }) }),
522
- idpLoginEnabled && /* @__PURE__ */ jsxRuntime.jsxs(core.Flex, { flexDirection: "column", gap: true, children: [
523
- /* @__PURE__ */ jsxRuntime.jsx(core.Text, { colorScheme: "light.700", appearance: "body2", style: { textAlign: "center" }, mb: 4, children: t.trial }),
524
- trialProviders == null ? void 0 : trialProviders.map((provider) => /* @__PURE__ */ jsxRuntime.jsx(
525
- ButtonProvider,
526
- {
527
- provider,
528
- login,
529
- loading: loading && loginProvider === provider
530
- },
531
- provider
532
- ))
533
- ] })
534
- ] }),
535
- error && /* @__PURE__ */ jsxRuntime.jsxs(core.Text, { className: "error", children: [
536
- t.error,
537
- ": ",
538
- error
539
- ] })
610
+ mode === "sso" ? /* @__PURE__ */ jsxRuntime.jsx(
611
+ SSOLogin,
612
+ {
613
+ disabled,
614
+ loading,
615
+ hasProvider: !loginProvider,
616
+ value: email,
617
+ onChange: setEmail,
618
+ onChangeMode: setMode
619
+ }
620
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
621
+ IDPLogin,
622
+ {
623
+ loading,
624
+ loginProvider,
625
+ onSubmit: login,
626
+ trialProviders,
627
+ onChangeMode: setMode
628
+ }
629
+ ),
630
+ error && /* @__PURE__ */ jsxRuntime.jsx(core.Text, { className: "error", align: "center", children: error })
540
631
  ] }),
541
632
  banner ? /* @__PURE__ */ jsxRuntime.jsx(portalComponents.BannerWarning, { children: banner }) : null
542
633
  ] });
543
634
  };
544
- const dictionary = {
545
- en: {
546
- welcome: "Welcome to StackSpot",
547
- loginWithEmail: "Log in with your email.",
548
- label: "Corporate email",
549
- placeholder: "email@company.com",
550
- continue: "Continue",
551
- or: "Or",
552
- loginWith: "Sign in with $0",
553
- error: "Error while attempting to login",
554
- emailNotAllowedTitle: "Your email is linked to an Enterprise account.",
555
- emailNotAllowedSubtitle: "Please log in with your corporate email.",
556
- trial: "Access your trial account"
557
- },
558
- pt: {
559
- welcome: "Bem vindo \xE0 StackSpot",
560
- loginWithEmail: "Fa\xE7a login com seu e-mail.",
561
- label: "Email corporativo",
562
- placeholder: "email@empresa.com",
563
- continue: "Continuar",
564
- or: "Ou",
565
- loginWith: "Entrar com $0",
566
- error: "Erro ao fazer login",
567
- emailNotAllowedTitle: '"Este e-mail est\xE1 vinculado a uma conta Enterprise.',
568
- emailNotAllowedSubtitle: "Fa\xE7a login com seu email corporativo.",
569
- trial: "Acesse sua conta de teste"
570
- }
571
- };
572
635
 
573
636
  var __defProp = Object.defineProperty;
574
637
  var __defProps = Object.defineProperties;