@stack-spot/auth-react 2.14.1-beta.1 → 2.14.1

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/IDPLogin.tsx CHANGED
@@ -1,103 +1,103 @@
1
- import { Box, Button, Flex, IconBox, Text } from '@citric/core'
2
- import { ExclamationTriangle } from '@citric/icons'
3
- import { LoadingCircular } from '@citric/ui'
4
- import { interpolate } from '@stack-spot/portal-translate'
5
- import { capitalize } from 'lodash'
6
- import { useTranslation } from './dictionary'
7
- import { Provider } from './hooks'
8
- import { Github } from './provider-icons/Github'
9
- import { Google } from './provider-icons/Google'
10
- import { Microsoft } from './provider-icons/Microsoft'
11
- import { LoginType } from './types'
12
-
13
- interface Props {
14
- trialProviders: Provider[],
15
- loading: boolean,
16
- loginProvider: Provider | undefined,
17
- onSubmit: (type: LoginType, provider?: Provider) => void,
18
- onChangeMode: (mode: LoginType) => void,
19
- }
20
-
21
- interface ButtonProviderProps {
22
- provider: Provider,
23
- loading: boolean,
24
- disabled?: boolean,
25
- login: (type: LoginType, provider?: Provider) => void,
26
- }
27
-
28
- const providerIcons: Record<Provider, React.ReactElement> = {
29
- github: <Github />,
30
- google: <Google />,
31
- microsoft: <Microsoft />,
32
- }
33
-
34
- const ButtonProvider = ({ provider, login, loading, disabled }: ButtonProviderProps) => {
35
- const t = useTranslation()
36
- return (
37
- <Box>
38
- <Button
39
- colorScheme="light"
40
- type="button"
41
- size='md'
42
- sx={{ width: '100%' } as any}
43
- onClick={() => login('idp', provider)}
44
- disabled={loading || disabled}
45
- >
46
- {loading
47
- ? <LoadingCircular />
48
- : <Flex alignItems='center' style={{ gap: '4px' }}>
49
- {providerIcons[provider]}{interpolate(t.loginWith, capitalize(provider))}
50
- </Flex>
51
- }
52
- </Button>
53
- </Box>
54
- )
55
- }
56
-
57
- function hasTouchSupport() {
58
- return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
59
- }
60
-
61
- function isMobileUserAgent() {
62
- return /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
63
- }
64
-
65
- function isMobileWithDesktopView() {
66
- return !isMobileUserAgent() && hasTouchSupport();
67
- }
68
-
69
- export const IDPLogin = ({ trialProviders, loading, loginProvider, onSubmit, onChangeMode }: Props) => {
70
- const t = useTranslation()
71
- const showErrorMessage = isMobileWithDesktopView()
72
-
73
- return (
74
- <>
75
- {showErrorMessage && <Flex bg="warning" p={4} role="alert" flexWrap='nowrap'>
76
- <IconBox colorIcon="warning.contrastText">
77
- <ExclamationTriangle />
78
- </IconBox>
79
- <Text appearance='body2' ml={2} colorScheme="warning.contrastText">
80
- {t.errorMobileDesktop}
81
- </Text>
82
- </Flex>}
83
-
84
- <Flex flexDirection='column' gap>
85
- {trialProviders?.map((provider) => <ButtonProvider
86
- provider={provider}
87
- login={onSubmit}
88
- loading={loading && loginProvider === provider}
89
- disabled={loading}
90
- key={provider}
91
- />)
92
- }
93
- </Flex>
94
- <p className="separator">
95
- <Text appearance='microtext1' colorScheme='light.700'>{t.or}</Text>
96
- </p>
97
- <Text colorScheme="light.700" align="center">{t.corporateLoginTitle}</Text>
98
- <Button size='md' disabled={loading} colorScheme="light" onClick={() => onChangeMode('sso')}>
99
- {t.corporateLoginButton}
100
- </Button>
101
- </>
102
- )
1
+ import { Box, Button, Flex, IconBox, Text } from '@citric/core'
2
+ import { ExclamationTriangle } from '@citric/icons'
3
+ import { LoadingCircular } from '@citric/ui'
4
+ import { interpolate } from '@stack-spot/portal-translate'
5
+ import { capitalize } from 'lodash'
6
+ import { useTranslation } from './dictionary'
7
+ import { Provider } from './hooks'
8
+ import { Github } from './provider-icons/Github'
9
+ import { Google } from './provider-icons/Google'
10
+ import { Microsoft } from './provider-icons/Microsoft'
11
+ import { LoginType } from './types'
12
+
13
+ interface Props {
14
+ trialProviders: Provider[],
15
+ loading: boolean,
16
+ loginProvider: Provider | undefined,
17
+ onSubmit: (type: LoginType, provider?: Provider) => void,
18
+ onChangeMode: (mode: LoginType) => void,
19
+ }
20
+
21
+ interface ButtonProviderProps {
22
+ provider: Provider,
23
+ loading: boolean,
24
+ disabled?: boolean,
25
+ login: (type: LoginType, provider?: Provider) => void,
26
+ }
27
+
28
+ const providerIcons: Record<Provider, React.ReactElement> = {
29
+ github: <Github />,
30
+ google: <Google />,
31
+ microsoft: <Microsoft />,
32
+ }
33
+
34
+ const ButtonProvider = ({ provider, login, loading, disabled }: ButtonProviderProps) => {
35
+ const t = useTranslation()
36
+ return (
37
+ <Box>
38
+ <Button
39
+ colorScheme="light"
40
+ type="button"
41
+ size='md'
42
+ sx={{ width: '100%' } as any}
43
+ onClick={() => login('idp', provider)}
44
+ disabled={loading || disabled}
45
+ >
46
+ {loading
47
+ ? <LoadingCircular />
48
+ : <Flex alignItems='center' style={{ gap: '4px' }}>
49
+ {providerIcons[provider]}{interpolate(t.loginWith, capitalize(provider))}
50
+ </Flex>
51
+ }
52
+ </Button>
53
+ </Box>
54
+ )
55
+ }
56
+
57
+ function hasTouchSupport() {
58
+ return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
59
+ }
60
+
61
+ function isMobileUserAgent() {
62
+ return /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
63
+ }
64
+
65
+ function isMobileWithDesktopView() {
66
+ return !isMobileUserAgent() && hasTouchSupport();
67
+ }
68
+
69
+ export const IDPLogin = ({ trialProviders, loading, loginProvider, onSubmit, onChangeMode }: Props) => {
70
+ const t = useTranslation()
71
+ const showErrorMessage = isMobileWithDesktopView()
72
+
73
+ return (
74
+ <>
75
+ {showErrorMessage && <Flex bg="warning" p={4} role="alert" flexWrap='nowrap'>
76
+ <IconBox colorIcon="warning.contrastText">
77
+ <ExclamationTriangle />
78
+ </IconBox>
79
+ <Text appearance='body2' ml={2} colorScheme="warning.contrastText">
80
+ {t.errorMobileDesktop}
81
+ </Text>
82
+ </Flex>}
83
+
84
+ <Flex flexDirection='column' gap>
85
+ {trialProviders?.map((provider) => <ButtonProvider
86
+ provider={provider}
87
+ login={onSubmit}
88
+ loading={loading && loginProvider === provider}
89
+ disabled={loading}
90
+ key={provider}
91
+ />)
92
+ }
93
+ </Flex>
94
+ <p className="separator">
95
+ <Text appearance='microtext1' colorScheme='light.700'>{t.or}</Text>
96
+ </p>
97
+ <Text colorScheme="light.700" align="center">{t.corporateLoginTitle}</Text>
98
+ <Button size='md' disabled={loading} colorScheme="light" onClick={() => onChangeMode('sso')}>
99
+ {t.corporateLoginButton}
100
+ </Button>
101
+ </>
102
+ )
103
103
  }
package/src/Login.tsx CHANGED
@@ -1,206 +1,206 @@
1
- import { Box, Flex, Text } from '@citric/core'
2
- import { Card, LoadingCircular } from '@citric/ui'
3
- import { AuthMethodUnavailable } from '@stack-spot/auth'
4
- import { BannerWarning } from '@stack-spot/portal-components'
5
- import { MiniLogo } from '@stack-spot/portal-components/svg'
6
- import { theme } from '@stack-spot/portal-theme'
7
- import { CSSProperties, useEffect, useState } from 'react'
8
- import { styled } from 'styled-components'
9
- import { useTranslation } from './dictionary'
10
- import { Provider, useTrialProviders } from './hooks'
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'
15
-
16
- const LoginBox = styled.form`
17
- display: flex;
18
- flex-direction: column;
19
- justify-content: center;
20
- gap: 24px;
21
-
22
- header {
23
- display: flex;
24
- flex-direction: column;
25
- align-items: center;
26
- gap: 24px;
27
- }
28
-
29
- .separator {
30
- padding: 0 8px;
31
- display: flex;
32
- flex-direction: row;
33
- align-items: center;
34
- justify-content: center;
35
- gap: 8px;
36
- margin: 0;
37
-
38
- &:before, &:after {
39
- content: '';
40
- height: 1px;
41
- flex: 1;
42
- background-color: ${theme.color.light['500']};
43
- }
44
- }
45
-
46
- .error {
47
- color: ${theme.color.danger['500']};
48
- line-height: 1.5rem;
49
- }
50
- `
51
-
52
- const EmailNotAllowed = () => {
53
- const t = useTranslation()
54
- return <Card>
55
- <Flex justifyContent="center">
56
- <Text appearance='body2'>
57
- {t.emailNotAllowedTitle}
58
- </Text>
59
- <Text appearance='body2' colorScheme='light.700'>
60
- {t.emailNotAllowedSubtitle}
61
- </Text>
62
- </Flex>
63
- </Card>
64
- }
65
-
66
- const stylesBanner: CSSProperties = {
67
- position: 'fixed',
68
- top: 0,
69
- left: 0,
70
- width: '100%',
71
- }
72
-
73
- export const Login = (
74
- { onSubmit, initialValue = '', showLogin = true, welcomeText, removeLoadingOnSuccess, className, style, banner, loginTypes = ['idp', 'sso'] }: LoginProps,
75
- ) => {
76
- const t = useTranslation()
77
- const [trialProviders, isLoadingTrialProviders] = useTrialProviders({ enabled: loginTypes.includes('idp') })
78
- const searchParams = new URLSearchParams(location.search)
79
- const [error, setError] = useState(searchParams.get('error_description') || searchParams.get('error') || '')
80
- const [errorCode] = useState(searchParams.get('error_code') || '')
81
- const [loading, setLoading] = useState(false)
82
- const providerQueryParam = searchParams.get('provider') as Provider & 'email'
83
- const [loginProvider, setLoginProvider] = useState<Provider | undefined>()
84
- const [email, setEmail] = useState(initialValue || searchParams.get('email') || '')
85
- const disabled = !email.match(/\w+@\w+/)
86
- const idpLoginEnabled = loginTypes.includes('idp') && !!trialProviders?.length
87
- const [mode, setMode] = useState<LoginType | undefined>()
88
-
89
- useEffect(() => {
90
- setMode(idpLoginEnabled ? getLastLoginType() : 'sso')
91
- }, [idpLoginEnabled])
92
-
93
- useEffect(() => {
94
- if (!providerQueryParam) return
95
- if (providerQueryParam === 'email') login('sso')
96
- else if (trialProviders?.includes(providerQueryParam)) login('idp', providerQueryParam)
97
- }, [trialProviders, isLoadingTrialProviders])
98
-
99
- async function login(type: LoginType, provider?: Provider) {
100
- setError('')
101
- setLoading(true)
102
- provider !== loginProvider && setLoginProvider(provider)
103
- try {
104
- const data: LoginData = type === 'idp' && !!provider ? { type: 'idp', provider: `external-idp:${provider}` } : { type: 'sso', email }
105
- setLastLoginType(data.type)
106
- await onSubmit(data)
107
- if (removeLoadingOnSuccess) setLoading(false)
108
- } catch (error: any) {
109
- setLoading(false)
110
- setLoginProvider(undefined)
111
- if (error instanceof AuthMethodUnavailable) {
112
- setError(t.emailNotFoundError)
113
- } else {
114
- setError(error.message || error.toString())
115
- }
116
- }
117
- }
118
-
119
- useEffect(() => {
120
- const meta = document.createElement('meta')
121
- meta.name = 'adopt-website-id'
122
- meta.content = 'a32fe656-2333-4dc5-8039-dddac6464587'
123
- document.head.appendChild(meta)
124
-
125
- const script = document.createElement('script')
126
- script.src = '//tag.goadopt.io/injector.js?website_code=a32fe656-2333-4dc5-8039-dddac6464587'
127
- script.className = 'adopt-injector'
128
- script.async = true
129
- document.body.appendChild(script)
130
-
131
- return () => {
132
- document.head.removeChild(meta)
133
- document.body.removeChild(script)
134
- }
135
- }, [])
136
-
137
- function submitForm(e: React.FormEvent<HTMLFormElement>) {
138
- e.preventDefault()
139
- if (disabled) return
140
- login('sso')
141
- }
142
-
143
- if (isLoadingTrialProviders || !mode) {
144
- return <Flex alignContent="center" justifyContent="center" my={5}><LoadingCircular /> </Flex>
145
- }
146
-
147
- const loginWithSocialAccount = (
148
- <>
149
- <span>{t.loginWithSocialAccount1}</span>
150
- <span style={{ fontWeight: 'bold' }}>{t.loginWithSocialAccount2}</span>
151
- <span>{t.loginWithSocialAccount3}</span>
152
- </>
153
- )
154
-
155
- return (
156
- <>
157
- {banner && <Box style={stylesBanner}>
158
- <BannerWarning showCloseButton={false}>
159
- {banner}
160
- </BannerWarning>
161
- </Box>}
162
- {showLogin ? (
163
- <LoginBox onSubmit={submitForm} className={className} style={style}>
164
- <header>
165
- <MiniLogo />
166
- <Flex flexDirection='column' alignItems='center'>
167
- <Text appearance='body1' weight='medium'>{welcomeText || t.welcome}</Text>
168
- <Text appearance='body2' colorScheme='light.700' align='center'>
169
- {mode === 'idp' ? loginWithSocialAccount : t.loginWithEmail}
170
- </Text>
171
- </Flex>
172
- </header>
173
- {errorCode && errorCode === 'EMAIL_IS_NOT_ALLOWED' && <EmailNotAllowed />}
174
-
175
- {mode === 'sso' ? (
176
- <SSOLogin
177
- disabled={disabled}
178
- loading={loading}
179
- hasProvider={!loginProvider}
180
- value={email}
181
- onChange={setEmail}
182
- onChangeMode={setMode}
183
- idpLoginEnabled={idpLoginEnabled}
184
- />
185
- ) : (
186
- <IDPLogin
187
- loading={loading}
188
- loginProvider={loginProvider}
189
- onSubmit={login}
190
- trialProviders={trialProviders}
191
- onChangeMode={setMode}
192
- />
193
- )}
194
-
195
- {error && <Text className="error" align="center">{error}</Text>}
196
- </LoginBox>
197
- ) : (
198
- <Flex flexDirection='column' alignItems='center'>
199
- <MiniLogo />
200
- <Text appearance='body1' weight='medium'>{welcomeText || t.welcome}</Text>
201
- <Text mt={4}>{t.loginTemporarilyUnavailable}</Text>
202
- </Flex>
203
- )}
204
- </>
205
- )
206
- }
1
+ import { Box, Flex, Text } from '@citric/core'
2
+ import { Card, LoadingCircular } from '@citric/ui'
3
+ import { AuthMethodUnavailable } from '@stack-spot/auth'
4
+ import { BannerWarning } from '@stack-spot/portal-components'
5
+ import { MiniLogo } from '@stack-spot/portal-components/svg'
6
+ import { theme } from '@stack-spot/portal-theme'
7
+ import { CSSProperties, useEffect, useState } from 'react'
8
+ import { styled } from 'styled-components'
9
+ import { useTranslation } from './dictionary'
10
+ import { Provider, useTrialProviders } from './hooks'
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'
15
+
16
+ const LoginBox = styled.form`
17
+ display: flex;
18
+ flex-direction: column;
19
+ justify-content: center;
20
+ gap: 24px;
21
+
22
+ header {
23
+ display: flex;
24
+ flex-direction: column;
25
+ align-items: center;
26
+ gap: 24px;
27
+ }
28
+
29
+ .separator {
30
+ padding: 0 8px;
31
+ display: flex;
32
+ flex-direction: row;
33
+ align-items: center;
34
+ justify-content: center;
35
+ gap: 8px;
36
+ margin: 0;
37
+
38
+ &:before, &:after {
39
+ content: '';
40
+ height: 1px;
41
+ flex: 1;
42
+ background-color: ${theme.color.light['500']};
43
+ }
44
+ }
45
+
46
+ .error {
47
+ color: ${theme.color.danger['500']};
48
+ line-height: 1.5rem;
49
+ }
50
+ `
51
+
52
+ const EmailNotAllowed = () => {
53
+ const t = useTranslation()
54
+ return <Card>
55
+ <Flex justifyContent="center">
56
+ <Text appearance='body2'>
57
+ {t.emailNotAllowedTitle}
58
+ </Text>
59
+ <Text appearance='body2' colorScheme='light.700'>
60
+ {t.emailNotAllowedSubtitle}
61
+ </Text>
62
+ </Flex>
63
+ </Card>
64
+ }
65
+
66
+ const stylesBanner: CSSProperties = {
67
+ position: 'fixed',
68
+ top: 0,
69
+ left: 0,
70
+ width: '100%',
71
+ }
72
+
73
+ export const Login = (
74
+ { onSubmit, initialValue = '', showLogin = true, welcomeText, removeLoadingOnSuccess, className, style, banner, loginTypes = ['idp', 'sso'] }: LoginProps,
75
+ ) => {
76
+ const t = useTranslation()
77
+ const [trialProviders, isLoadingTrialProviders] = useTrialProviders({ enabled: loginTypes.includes('idp') })
78
+ const searchParams = new URLSearchParams(location.search)
79
+ const [error, setError] = useState(searchParams.get('error_description') || searchParams.get('error') || '')
80
+ const [errorCode] = useState(searchParams.get('error_code') || '')
81
+ const [loading, setLoading] = useState(false)
82
+ const providerQueryParam = searchParams.get('provider') as Provider & 'email'
83
+ const [loginProvider, setLoginProvider] = useState<Provider | undefined>()
84
+ const [email, setEmail] = useState(initialValue || searchParams.get('email') || '')
85
+ const disabled = !email.match(/\w+@\w+/)
86
+ const idpLoginEnabled = loginTypes.includes('idp') && !!trialProviders?.length
87
+ const [mode, setMode] = useState<LoginType | undefined>()
88
+
89
+ useEffect(() => {
90
+ setMode(idpLoginEnabled ? getLastLoginType() : 'sso')
91
+ }, [idpLoginEnabled])
92
+
93
+ useEffect(() => {
94
+ if (!providerQueryParam) return
95
+ if (providerQueryParam === 'email') login('sso')
96
+ else if (trialProviders?.includes(providerQueryParam)) login('idp', providerQueryParam)
97
+ }, [trialProviders, isLoadingTrialProviders])
98
+
99
+ async function login(type: LoginType, provider?: Provider) {
100
+ setError('')
101
+ setLoading(true)
102
+ provider !== loginProvider && setLoginProvider(provider)
103
+ try {
104
+ const data: LoginData = type === 'idp' && !!provider ? { type: 'idp', provider: `external-idp:${provider}` } : { type: 'sso', email }
105
+ setLastLoginType(data.type)
106
+ await onSubmit(data)
107
+ if (removeLoadingOnSuccess) setLoading(false)
108
+ } catch (error: any) {
109
+ setLoading(false)
110
+ setLoginProvider(undefined)
111
+ if (error instanceof AuthMethodUnavailable) {
112
+ setError(t.emailNotFoundError)
113
+ } else {
114
+ setError(error.message || error.toString())
115
+ }
116
+ }
117
+ }
118
+
119
+ useEffect(() => {
120
+ const meta = document.createElement('meta')
121
+ meta.name = 'adopt-website-id'
122
+ meta.content = 'a32fe656-2333-4dc5-8039-dddac6464587'
123
+ document.head.appendChild(meta)
124
+
125
+ const script = document.createElement('script')
126
+ script.src = '//tag.goadopt.io/injector.js?website_code=a32fe656-2333-4dc5-8039-dddac6464587'
127
+ script.className = 'adopt-injector'
128
+ script.async = true
129
+ document.body.appendChild(script)
130
+
131
+ return () => {
132
+ document.head.removeChild(meta)
133
+ document.body.removeChild(script)
134
+ }
135
+ }, [])
136
+
137
+ function submitForm(e: React.FormEvent<HTMLFormElement>) {
138
+ e.preventDefault()
139
+ if (disabled) return
140
+ login('sso')
141
+ }
142
+
143
+ if (isLoadingTrialProviders || !mode) {
144
+ return <Flex alignContent="center" justifyContent="center" my={5}><LoadingCircular /> </Flex>
145
+ }
146
+
147
+ const loginWithSocialAccount = (
148
+ <>
149
+ <span>{t.loginWithSocialAccount1}</span>
150
+ <span style={{ fontWeight: 'bold' }}>{t.loginWithSocialAccount2}</span>
151
+ <span>{t.loginWithSocialAccount3}</span>
152
+ </>
153
+ )
154
+
155
+ return (
156
+ <>
157
+ {banner && <Box style={stylesBanner}>
158
+ <BannerWarning showCloseButton={false}>
159
+ {banner}
160
+ </BannerWarning>
161
+ </Box>}
162
+ {showLogin ? (
163
+ <LoginBox onSubmit={submitForm} className={className} style={style}>
164
+ <header>
165
+ <MiniLogo />
166
+ <Flex flexDirection='column' alignItems='center'>
167
+ <Text appearance='body1' weight='medium'>{welcomeText || t.welcome}</Text>
168
+ <Text appearance='body2' colorScheme='light.700' align='center'>
169
+ {mode === 'idp' ? loginWithSocialAccount : t.loginWithEmail}
170
+ </Text>
171
+ </Flex>
172
+ </header>
173
+ {errorCode && errorCode === 'EMAIL_IS_NOT_ALLOWED' && <EmailNotAllowed />}
174
+
175
+ {mode === 'sso' ? (
176
+ <SSOLogin
177
+ disabled={disabled}
178
+ loading={loading}
179
+ hasProvider={!loginProvider}
180
+ value={email}
181
+ onChange={setEmail}
182
+ onChangeMode={setMode}
183
+ idpLoginEnabled={idpLoginEnabled}
184
+ />
185
+ ) : (
186
+ <IDPLogin
187
+ loading={loading}
188
+ loginProvider={loginProvider}
189
+ onSubmit={login}
190
+ trialProviders={trialProviders}
191
+ onChangeMode={setMode}
192
+ />
193
+ )}
194
+
195
+ {error && <Text className="error" align="center">{error}</Text>}
196
+ </LoginBox>
197
+ ) : (
198
+ <Flex flexDirection='column' alignItems='center'>
199
+ <MiniLogo />
200
+ <Text appearance='body1' weight='medium'>{welcomeText || t.welcome}</Text>
201
+ <Text mt={4}>{t.loginTemporarilyUnavailable}</Text>
202
+ </Flex>
203
+ )}
204
+ </>
205
+ )
206
+ }