@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/CHANGELOG.md +14 -0
- package/out/index.d.ts +26 -25
- package/out/index.js +157 -98
- package/out/index.js.map +1 -1
- package/out/index.mjs +158 -99
- package/out/index.mjs.map +1 -1
- package/package.json +1 -1
- package/rollup.config.mjs +1 -1
- package/src/Authenticated.tsx +2 -1
- package/src/IDPLogin.tsx +79 -0
- package/src/Login.tsx +48 -144
- package/src/SSOLogin.tsx +50 -0
- package/src/dictionary.ts +40 -0
- package/src/hooks.ts +8 -3
- package/src/index.ts +0 -1
- package/src/last-login-type.ts +14 -0
- package/src/types.ts +28 -0
package/src/Login.tsx
CHANGED
|
@@ -1,46 +1,17 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
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 =
|
|
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 = (
|
|
129
|
-
loginTypes = ['idp', 'sso'] }: LoginProps
|
|
130
|
-
|
|
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
|
|
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
|
-
|
|
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'>
|
|
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
|
-
{
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
{
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
package/src/SSOLogin.tsx
ADDED
|
@@ -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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
@@ -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
|
+
}
|