@stack-spot/auth-react 2.7.0 → 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/src/Login.tsx CHANGED
@@ -1,46 +1,17 @@
1
- import { Box, Button, Flex, Input, Label, Text } from '@citric/core'
1
+ import { Flex, Text } from '@citric/core'
2
2
  import { Card, LoadingCircular } from '@citric/ui'
3
+ import { AuthMethodUnavailable } from '@stack-spot/auth'
3
4
  import { BannerWarning } from '@stack-spot/portal-components'
4
5
  import { MiniLogo } from '@stack-spot/portal-components/svg'
5
6
  import { theme } from '@stack-spot/portal-theme'
6
- import { Dictionary, interpolate, useTranslate } from '@stack-spot/portal-translate'
7
7
  import { useEffect, useState } from 'react'
8
8
  import { styled } from 'styled-components'
9
+ import { useTranslation } from './dictionary'
9
10
  import { Provider, useTrialProviders } from './hooks'
10
- import { Github } from './provider-icons/Github'
11
- import { Google } from './provider-icons/Google'
12
- import { Microsoft } from './provider-icons/Microsoft'
13
-
14
- export type LoginType = 'sso' | 'idp'
15
-
16
-
17
-
18
- interface BaseData {
19
- type: LoginType,
20
- }
21
-
22
- interface SSOData extends BaseData {
23
- type: 'sso',
24
- email: string,
25
- }
26
-
27
- interface IDPData extends BaseData {
28
- type: 'idp',
29
- provider: 'external-idp:github' | 'external-idp:google' | 'external-idp:microsoft',
30
- }
31
-
32
- type LoginData = SSOData | IDPData
33
-
34
- export type LoginProps = {
35
- initialValue?: string,
36
- onSubmit: (data: LoginData) => Promise<void>,
37
- welcomeText?: string,
38
- removeLoadingOnSuccess?: boolean,
39
- className?: string,
40
- style?: React.CSSProperties,
41
- banner?: React.ReactNode,
42
- loginTypes?: LoginType[],
43
- }
11
+ import { IDPLogin } from './IDPLogin'
12
+ import { getLastLoginType, setLastLoginType } from './last-login-type'
13
+ import { SSOLogin } from './SSOLogin'
14
+ import { LoginData, LoginProps, LoginType } from './types'
44
15
 
45
16
  const LoginBox = styled.form`
46
17
  display: flex;
@@ -78,41 +49,8 @@ const LoginBox = styled.form`
78
49
  }
79
50
  `
80
51
 
81
- const providerIcons: Record<Provider, React.ReactElement> = {
82
- github: <Github />,
83
- google: <Google />,
84
- microsoft: <Microsoft />,
85
- }
86
-
87
- interface ButtonProvider {
88
- provider: Provider,
89
- loading: boolean,
90
- login: (type: LoginType, provider?: Provider) => void
91
- }
92
-
93
- function capitalize(str: string) {
94
- return str.charAt(0).toUpperCase() + str.slice(1)
95
- }
96
-
97
- const ButtonProvider = ({ provider, login, loading }: ButtonProvider) => {
98
- const t = useTranslate(dictionary)
99
- return (
100
- <Box>
101
- <Button colorScheme="light" type="button" size='md' sx={{ width: '100%' } as any} onClick={() => login('idp', provider)} disabled={loading}>
102
- {loading
103
- ? <LoadingCircular />
104
- : <Flex alignItems='center' style={{ gap: '4px' }}>
105
- {providerIcons[provider]}{interpolate(t.loginWithGithub, capitalize(provider))}
106
- </Flex>
107
- }
108
- </Button>
109
- </Box>
110
- )
111
- }
112
-
113
-
114
52
  const EmailNotAllowed = () => {
115
- const t = useTranslate(dictionary)
53
+ const t = useTranslation()
116
54
  return <Card>
117
55
  <Flex justifyContent="center">
118
56
  <Text appearance='body2'>
@@ -125,9 +63,10 @@ const EmailNotAllowed = () => {
125
63
  </Card>
126
64
  }
127
65
 
128
- export const Login = ({ onSubmit, initialValue = '', welcomeText, removeLoadingOnSuccess, className, style, banner,
129
- loginTypes = ['idp', 'sso'] }: LoginProps) => {
130
- const t = useTranslate(dictionary)
66
+ export const Login = (
67
+ { onSubmit, initialValue = '', welcomeText, removeLoadingOnSuccess, className, style, banner, loginTypes = ['idp', 'sso'] }: LoginProps,
68
+ ) => {
69
+ const t = useTranslation()
131
70
  const [trialProviders, isLoadingTrialProviders] = useTrialProviders({ enabled: loginTypes.includes('idp') })
132
71
  const searchParams = new URLSearchParams(location.search)
133
72
  const [error, setError] = useState(searchParams.get('error_description') || searchParams.get('error') || '')
@@ -138,7 +77,11 @@ export const Login = ({ onSubmit, initialValue = '', welcomeText, removeLoadingO
138
77
  const [email, setEmail] = useState(initialValue || searchParams.get('email') || '')
139
78
  const disabled = !email.match(/\w+@\w+/)
140
79
  const idpLoginEnabled = loginTypes.includes('idp') && !!trialProviders?.length
141
- const ssoLoginEnabled = loginTypes.includes('sso')
80
+ const [mode, setMode] = useState<LoginType | undefined>()
81
+
82
+ useEffect(() => {
83
+ setMode(idpLoginEnabled ? getLastLoginType() : 'sso')
84
+ }, [idpLoginEnabled])
142
85
 
143
86
  useEffect(() => {
144
87
  if (!providerQueryParam) return
@@ -152,12 +95,17 @@ export const Login = ({ onSubmit, initialValue = '', welcomeText, removeLoadingO
152
95
  provider !== loginProvider && setLoginProvider(provider)
153
96
  try {
154
97
  const data: LoginData = type === 'idp' && !!provider ? { type: 'idp', provider: `external-idp:${provider}` } : { type: 'sso', email }
98
+ setLastLoginType(data.type)
155
99
  await onSubmit(data)
156
100
  if (removeLoadingOnSuccess) setLoading(false)
157
101
  } catch (error: any) {
158
102
  setLoading(false)
159
103
  setLoginProvider(undefined)
160
- setError(error.message || error.toString())
104
+ if (error instanceof AuthMethodUnavailable) {
105
+ setError(t.emailNotFoundError)
106
+ } else {
107
+ setError(error.message || error.toString())
108
+ }
161
109
  }
162
110
  }
163
111
 
@@ -167,6 +115,10 @@ export const Login = ({ onSubmit, initialValue = '', welcomeText, removeLoadingO
167
115
  login('sso')
168
116
  }
169
117
 
118
+ if (isLoadingTrialProviders || !mode) {
119
+ return <Flex alignContent="center" justifyContent="center" my={5}><LoadingCircular /> </Flex>
120
+ }
121
+
170
122
  return (
171
123
  <>
172
124
  <LoginBox onSubmit={submitForm} className={className} style={style}>
@@ -174,45 +126,31 @@ export const Login = ({ onSubmit, initialValue = '', welcomeText, removeLoadingO
174
126
  <MiniLogo />
175
127
  <Flex flexDirection='column' alignItems='center'>
176
128
  <Text appearance='body1' weight='medium'>{welcomeText || t.welcome}</Text>
177
- <Text appearance='body2' colorScheme='light.700'> {t.loginWithEmail}</Text>
129
+ <Text appearance='body2' colorScheme='light.700'>{mode === 'idp' ? t.loginWithSocialAccount : t.loginWithEmail}</Text>
178
130
  </Flex>
179
131
  </header>
180
132
  {errorCode && errorCode === 'EMAIL_IS_NOT_ALLOWED' && <EmailNotAllowed />}
181
- {ssoLoginEnabled && <>
182
- <Flex flexDirection='column' style={{ gap: '4px', marginTop: '4px' }}>
183
- <Label htmlFor='email'>{t.label}</Label>
184
- <Input type='email' name="email" value={email} onChange={e => setEmail(e.target.value)} placeholder={t.placeholder} />
185
- <Button colorScheme="primary" size='md' style={{ marginTop: '12px' }} disabled={disabled || loading}>
186
- {loading && !loginProvider ? <LoadingCircular /> : <Text>{t.continue}</Text>}
187
- </Button>
188
- </Flex>
189
- </>}
190
133
 
191
- {isLoadingTrialProviders
192
- ? <Flex alignContent="center" justifyContent="center" my={5}> <LoadingCircular /> </Flex>
193
- : <>
194
- {ssoLoginEnabled && idpLoginEnabled &&
195
- <p className="separator">
196
- <Text appearance='microtext1' colorScheme='light.700'>{t.or}</Text>
197
- </p>
198
- }
199
-
200
- {idpLoginEnabled && <Flex flexDirection='column' gap>
201
- <Text colorScheme='light.700' appearance='body2' style={{ textAlign: 'center' }} mb={4}>
202
- {t.trial}
203
- </Text>
204
-
205
- {trialProviders?.map((provider) => <ButtonProvider
206
- provider={provider}
207
- login={login}
208
- loading={loading && loginProvider === provider}
209
- key={provider}
210
- />)
211
- }
212
- </Flex>}
213
- </>
214
- }
215
- {error && <Text className="error">{t.error}: {error}</Text>}
134
+ {mode === 'sso' ? (
135
+ <SSOLogin
136
+ disabled={disabled}
137
+ loading={loading}
138
+ hasProvider={!loginProvider}
139
+ value={email}
140
+ onChange={setEmail}
141
+ onChangeMode={setMode}
142
+ />
143
+ ) : (
144
+ <IDPLogin
145
+ loading={loading}
146
+ loginProvider={loginProvider}
147
+ onSubmit={login}
148
+ trialProviders={trialProviders}
149
+ onChangeMode={setMode}
150
+ />
151
+ )}
152
+
153
+ {error && <Text className="error" align="center">{error}</Text>}
216
154
  </LoginBox>
217
155
  {banner ? <BannerWarning>
218
156
  {banner}
@@ -220,37 +158,3 @@ export const Login = ({ onSubmit, initialValue = '', welcomeText, removeLoadingO
220
158
  </>
221
159
  )
222
160
  }
223
-
224
-
225
- const dictionary = {
226
- en: {
227
- welcome: 'Welcome to StackSpot',
228
- loginWithEmail: 'Log in with your email.',
229
- label: 'Corporate email',
230
- placeholder: 'email@company.com',
231
- continue: 'Continue',
232
- or: 'Or',
233
- loginWithGithub: 'Sign in with Github',
234
- loginWithGoogle: 'Sign in with Google',
235
- loginWithMicrosoft: 'Sign in with Microsoft',
236
- error: 'Error while attempting to login',
237
- emailNotAllowedTitle: 'Your email is linked to an Enterprise account.',
238
- emailNotAllowedSubtitle: "Please log in with your corporate email.",
239
- trial: 'Access your trial account',
240
- },
241
- pt: {
242
- welcome: 'Bem vindo à StackSpot',
243
- loginWithEmail: 'Faça login com seu e-mail.',
244
- label: 'Email corporativo',
245
- placeholder: 'email@empresa.com',
246
- continue: 'Continuar',
247
- or: 'Ou',
248
- loginWithGithub: 'Entrar com o GitHub',
249
- loginWithGoogle: 'Entrar com Google',
250
- loginWithMicrosoft: 'Entrar com Microsoft',
251
- error: 'Erro ao fazer login',
252
- emailNotAllowedTitle: '"Este e-mail está vinculado a uma conta Enterprise.',
253
- emailNotAllowedSubtitle: "Faça login com seu email corporativo.",
254
- trial: 'Acesse sua conta de teste',
255
- },
256
- } satisfies Dictionary
@@ -0,0 +1,50 @@
1
+ import { Button, Flex, Input, Label, Text } from '@citric/core'
2
+ import { Github, Google } from '@citric/icons'
3
+ import { LoadingCircular } from '@citric/ui'
4
+ import { useTranslation } from './dictionary'
5
+ import { Provider } from './hooks'
6
+ import { Microsoft } from './provider-icons/Microsoft'
7
+ import { LoginType } from './types'
8
+
9
+ interface Props {
10
+ value: string,
11
+ onChange: (value: string) => void,
12
+ disabled: boolean,
13
+ loading: boolean,
14
+ hasProvider: boolean,
15
+ onChangeMode: (mode: LoginType) => void,
16
+ }
17
+
18
+ interface ButtonProviderProps {
19
+ provider: Provider,
20
+ loading: boolean,
21
+ login: (type: LoginType, provider?: Provider) => void
22
+ }
23
+
24
+ const providerIcons: Record<Provider, React.ReactElement> = {
25
+ github: <Github />,
26
+ google: <Google />,
27
+ microsoft: <Microsoft />,
28
+ }
29
+
30
+ export const SSOLogin = ({ value, onChange, loading, disabled, hasProvider, onChangeMode }: Props) => {
31
+ const t = useTranslation()
32
+ return (
33
+ <>
34
+ <Flex flexDirection='column' style={{ gap: '4px', marginTop: '4px' }}>
35
+ <Label htmlFor='email'>{t.label}</Label>
36
+ <Input id="email" type='email' name="email" value={value} onChange={e => onChange(e.target.value)} placeholder={t.placeholder} />
37
+ <Button colorScheme="primary" size='md' style={{ marginTop: '12px' }} disabled={disabled || loading}>
38
+ {loading && !hasProvider ? <LoadingCircular /> : <Text>{t.continue}</Text>}
39
+ </Button>
40
+ </Flex>
41
+ <p className="separator">
42
+ <Text appearance='microtext1' colorScheme='light.700'>{t.or}</Text>
43
+ </p>
44
+ <Text colorScheme="light.700" align="center">{t.socialLoginTitle}</Text>
45
+ <Button size='md' disabled={loading} colorScheme="light" onClick={() => onChangeMode('idp')}>
46
+ {t.socialLogin}
47
+ </Button>
48
+ </>
49
+ )
50
+ }
@@ -0,0 +1,40 @@
1
+ import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
2
+
3
+ const dictionary = {
4
+ en: {
5
+ welcome: 'Welcome to StackSpot',
6
+ loginWithEmail: 'Log in with your email.',
7
+ loginWithSocialAccount: 'Sign up or access your free trial with a social account',
8
+ label: 'Corporate email',
9
+ placeholder: 'email@company.com',
10
+ continue: 'Continue',
11
+ or: 'Or',
12
+ loginWith: 'Sign in with $0',
13
+ emailNotAllowedTitle: 'Your email is linked to an Enterprise account.',
14
+ emailNotAllowedSubtitle: "Please log in with your corporate email.",
15
+ socialLogin: 'Login or register with a social account',
16
+ corporateLoginTitle: 'Already have a StackSpot enterprise account?',
17
+ corporateLoginButton: 'Login with enterprise account',
18
+ socialLoginTitle: 'Do you want to access another way?',
19
+ emailNotFoundError: 'We couldn\'t find an account for this email',
20
+ },
21
+ pt: {
22
+ welcome: 'Boas vindas à StackSpot',
23
+ loginWithEmail: 'Faça login com seu e-mail.',
24
+ loginWithSocialAccount: 'Cadastre-se ou acesse seu teste gratuito com uma conta social',
25
+ label: 'Email corporativo',
26
+ placeholder: 'email@empresa.com',
27
+ continue: 'Continuar',
28
+ or: 'Ou',
29
+ loginWith: 'Entrar com $0',
30
+ emailNotAllowedTitle: '"Este e-mail está vinculado a uma conta Enterprise.',
31
+ emailNotAllowedSubtitle: "Faça login com seu email corporativo.",
32
+ socialLogin: 'Entre ou cadastre-se com uma conta social',
33
+ corporateLoginTitle: 'Já possui uma conta StackSpot Enterprise?',
34
+ corporateLoginButton: 'Entrar na conta Enterprise',
35
+ socialLoginTitle: 'Você quer entrar de outro jeito?',
36
+ emailNotFoundError: 'Não encontramos uma conta para este e-mail.',
37
+ },
38
+ } satisfies Dictionary
39
+
40
+ export const useTranslation = () => useTranslate(dictionary)
package/src/hooks.ts CHANGED
@@ -19,9 +19,14 @@ export const useTrialProviders = ({ enabled = true }): [Provider[], boolean] =>
19
19
  useEffect(() => {
20
20
  (async () => {
21
21
  if (!SessionManager.instance || !enabled) return
22
- const providers = (await SessionManager.instance.getTrialEnabledProviders()) as Provider[]
23
- setTrialProviders(providers)
24
- setIsLoadingTrialProviders(false)
22
+ try {
23
+ const providers = (await SessionManager.instance.getTrialEnabledProviders()) as Provider[]
24
+ setTrialProviders(providers)
25
+ setIsLoadingTrialProviders(false)
26
+ } catch (error) {
27
+ console.error(error)
28
+ setIsLoadingTrialProviders(false)
29
+ }
25
30
  })()
26
31
  }, [SessionManager.instance])
27
32
 
package/src/index.ts CHANGED
@@ -2,4 +2,3 @@ export { Authenticated } from './Authenticated'
2
2
  export { useSession } from './hooks'
3
3
  export { Login } from './Login'
4
4
  export { SessionManager } from './SessionManager'
5
-
@@ -0,0 +1,14 @@
1
+ import { LoginType } from './types'
2
+
3
+ const lastLoginTypeKey = 'lastLoginType'
4
+
5
+ export function getLastLoginType(): LoginType {
6
+ const type = localStorage.getItem(lastLoginTypeKey)
7
+ if (type === 'idp' || type === 'sso') return type
8
+ // for now, the user won't have the variable "lastLoginType" set. So, for now, we'll check if he/she already went through the tour.
9
+ return localStorage.getItem('guided-tour') ? 'sso' : 'idp'
10
+ }
11
+
12
+ export function setLastLoginType(type: LoginType) {
13
+ localStorage.setItem(lastLoginTypeKey, type)
14
+ }
package/src/types.ts ADDED
@@ -0,0 +1,28 @@
1
+ export type LoginType = 'sso' | 'idp'
2
+
3
+ interface BaseData {
4
+ type: LoginType,
5
+ }
6
+
7
+ interface SSOData extends BaseData {
8
+ type: 'sso',
9
+ email: string,
10
+ }
11
+
12
+ interface IDPData extends BaseData {
13
+ type: 'idp',
14
+ provider: 'external-idp:github' | 'external-idp:google' | 'external-idp:microsoft',
15
+ }
16
+
17
+ export type LoginData = SSOData | IDPData
18
+
19
+ export type LoginProps = {
20
+ initialValue?: string,
21
+ onSubmit: (data: LoginData) => Promise<void>,
22
+ welcomeText?: string,
23
+ removeLoadingOnSuccess?: boolean,
24
+ className?: string,
25
+ style?: React.CSSProperties,
26
+ banner?: React.ReactNode,
27
+ loginTypes?: LoginType[],
28
+ }