Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,17 @@
},
"companyServer": {
"title": "Login",
"body": "To log in and access your Twake, please enter its URL",
"body": {
"byUrl": "To log in and access your Twake, please enter its URL",
"byEmail": "To log in and access your Twake, please enter your company email address"
},
"toggle": {
"url": "Enter my Twake URL",
"email": "Enter my company email address"
},
"textFieldLabel": "Url",
"buttonLogin": "Next"
"buttonLogin": "Next",
"companyServerNotFound": "Company server not found"
}
},
"services": {
Expand Down
12 changes: 10 additions & 2 deletions src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,17 @@
},
"companyServer": {
"title": "Acceso",
"body": "Para conectarse y acceder a su Twake, introduzca su URL",
"body": {
"byUrl": "Para conectarse y acceder a su Twake, introduzca su URL",
"byEmail": "Para conectarse y acceder a su Twake, introduzca la dirección de correo electrónico corporativa"
},
"toggle": {
"url": "Introduce mi URL de Twake",
"email": "Introduzca la dirección de correo electrónico corporativa"
},
"textFieldLabel": "Url",
"buttonLogin": "Siguiente"
"buttonLogin": "Siguiente",
"companyServerNotFound": "No se ha encontrado el servidor de la corporativa"
}
},
"services": {
Expand Down
12 changes: 10 additions & 2 deletions src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,17 @@
},
"companyServer": {
"title": "Se connecter",
"body": "Pour vous connecter et accéder à votre Twake, veuillez saisir son URL",
"body": {
"byUrl": "Pour vous connecter et accéder à votre Twake, veuillez saisir son URL",
"byEmail": "Pour vous connecter et accéder à votre Twake, veuillez saisir votre adresse email d'entreprise"
},
"toggle": {
"url": "Entrer l'URL de mon Twake",
"email": "Entrer mon adresse email d'entreprise"
},
"textFieldLabel": "Serveur",
"buttonLogin": "Suivant"
"buttonLogin": "Suivant",
"companyServerNotFound": "Serveur d'entreprise introuvable"
}
},
"services": {
Expand Down
151 changes: 121 additions & 30 deletions src/screens/login/components/TwakeCustomServerView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ import { Left } from '/ui/Icons/Left'
import { Link } from '/ui/Link'
import { TextField } from '/ui/TextField'
import { Typography } from '/ui/Typography'
import { useHomeStateContext } from '/screens/home/HomeStateProvider'
import {
isOidcOnboardingStartCallback,
processOIDC
} from '/screens/login/components/functions/oidc'
import { useLoadingOverlay } from '/app/view/Loading/LoadingOverlayProvider'
import { getLoginUri } from '/screens/login/components/functions/autodiscovery'

const log = Minilog('TwakeCustomServerView')

Expand All @@ -38,16 +45,24 @@ interface TwakeCustomServerViewProps {
close: () => void
setInstanceData: setInstanceData
startOidcOauthNoCode: startOidcOauthNoCode
startOidcOAuth: startOidcOAuth
startOidcOnboarding: startOidcOnboarding
}

export const TwakeCustomServerView = ({
openTos,
close,
setInstanceData,
startOidcOauthNoCode
startOidcOauthNoCode,
startOidcOAuth,
startOidcOnboarding
}: TwakeCustomServerViewProps): JSX.Element => {
const [input, setInput] = useState('')
const [isLoginByEmail, setIsLoginByEmail] = useState(true)
const [urlInput, setUrlInput] = useState('')
const [emailInput, setEmailInput] = useState('')
const [error, setError] = useState<string | undefined>()
const { setOnboardedRedirection } = useHomeStateContext()
const { showOverlay, hideOverlay } = useLoadingOverlay()

const version = useMemo(() => getVersion(), [])

Expand All @@ -63,14 +78,23 @@ export const TwakeCustomServerView = ({
BackHandler.removeEventListener('hardwareBackPress', handleBackPress)
}, [handleBackPress])

const handleInput = (input: string): void => {
setInput(input)
const toggleLoginByEmail = (): void => {
setIsLoginByEmail(!isLoginByEmail)
setError(undefined)
}

const handleUrlInput = (input: string): void => {
setUrlInput(input)
}

const handleLogin = async (): Promise<void> => {
const handleEmailInput = (input: string): void => {
setEmailInput(input)
}

const handleLoginByUrl = async (): Promise<void> => {
setError(undefined)
try {
const sanitizedInput = sanitizeUrlInput(input)
const sanitizedInput = sanitizeUrlInput(urlInput)

const details = await fetchRegistrationDetails(new URL(sanitizedInput))

Expand Down Expand Up @@ -99,6 +123,34 @@ export const TwakeCustomServerView = ({
}
}

const handleLoginByEmail = async (): Promise<void> => {
setError(undefined)
try {
const loginUri = await getLoginUri(emailInput)

if (!loginUri) {
setError(t('screens.companyServer.companyServerNotFound'))
return
}

showOverlay()
const oidcResult = await processOIDC({ url: loginUri.toString() }, true)

if (isOidcOnboardingStartCallback(oidcResult)) {
void startOidcOnboarding(oidcResult.onboardUrl, oidcResult.code)
} else {
setOnboardedRedirection(oidcResult.defaultRedirection ?? '')
void startOidcOAuth(oidcResult.fqdn, oidcResult.code)
}
} catch (error: unknown) {
hideOverlay()
if (error !== 'USER_CANCELED') {
// @ts-expect-error error is always a valid type here
setError(getErrorMessage(error), error)
}
}
}

return (
<Container transparent={false}>
<Grid container direction="column" justifyContent="space-between">
Expand All @@ -112,29 +164,68 @@ export const TwakeCustomServerView = ({
<Typography variant="h2" color="textPrimary">
{t('screens.companyServer.title')}
</Typography>
<Typography
variant="body2"
color="textPrimary"
style={{
textAlign: 'center'
}}
>
{t('screens.companyServer.body')}
</Typography>
<TextField
style={styles.urlField}
label={t('screens.companyServer.textFieldLabel')}
onChangeText={handleInput}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onSubmitEditing={handleLogin}
returnKeyType="go"
value={input}
autoCapitalize="none"
autoFocus={false}
placeholder="https://claude.mycozy.cloud"
placeholderTextColor={colors.actionColorDisabled}
inputMode="url"
/>
{isLoginByEmail ? (
<>
<Typography
variant="body2"
color="textPrimary"
style={{
textAlign: 'center'
}}
>
{t('screens.companyServer.body.byEmail')}
</Typography>
<TextField
style={styles.urlField}
label="Email"
onChangeText={handleEmailInput}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onSubmitEditing={handleLoginByEmail}
returnKeyType="go"
value={emailInput}
autoCapitalize="none"
autoFocus={false}
placeholder="[email protected]"
placeholderTextColor={colors.actionColorDisabled}
inputMode="email"
/>
</>
) : (
<>
<Typography
variant="body2"
color="textPrimary"
style={{
textAlign: 'center'
}}
>
{t('screens.companyServer.body.byUrl')}
</Typography>
<TextField
style={styles.urlField}
label={t('screens.companyServer.textFieldLabel')}
onChangeText={handleUrlInput}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onSubmitEditing={handleLoginByUrl}
returnKeyType="go"
value={urlInput}
autoCapitalize="none"
autoFocus={false}
placeholder="https://claude.mycozy.cloud"
placeholderTextColor={colors.actionColorDisabled}
inputMode="url"
/>
</>
)}

<Link onPress={toggleLoginByEmail}>
<Typography variant="caption" color="primary">
{isLoginByEmail
? t('screens.companyServer.toggle.url')
: t('screens.companyServer.toggle.email')}
</Typography>
</Link>

{error && (
<Typography variant="body2" color="error">
{error}
Expand All @@ -146,7 +237,7 @@ export const TwakeCustomServerView = ({
<Grid alignItems="center" direction="column" style={styles.footerGrid}>
<Button
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onPress={handleLogin}
onPress={isLoginByEmail ? handleLoginByEmail : handleLoginByUrl}
variant="primary"
label={t('screens.companyServer.buttonLogin')}
style={styles.loginButton}
Expand Down
2 changes: 2 additions & 0 deletions src/screens/login/components/TwakeWelcomeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ export const TwakeWelcomeView = ({
openTos={openTos}
setInstanceData={setInstanceData}
startOidcOauthNoCode={startOidcOauthNoCode}
startOidcOAuth={startOidcOAuth}
startOidcOnboarding={startOidcOnboarding}
/>
)
}
Expand Down
79 changes: 79 additions & 0 deletions src/screens/login/components/functions/autodiscovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import strings from '/constants/strings.json'

interface TwakeConfiguration {
'twake-pass-login-uri'?: string
'twake-flagship-login-uri'?: string
}

export const extractDomain = (companyEmail: string): string | null => {
if (!companyEmail) {
return null
}

const email = companyEmail.trim()

const atIndex = email.lastIndexOf('@')

if (atIndex === -1) {
return null
}

return email.substring(atIndex + 1)
}

export const fetchLoginUriWithWellKnown = async (
domain: string
): Promise<URL | null> => {
const url = `https://${domain}/.well-known/twake-configuration`

try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
}
})

if (response.ok) {
const twakeConfiguration: TwakeConfiguration =
(await response.json()) as unknown as TwakeConfiguration

if (!twakeConfiguration['twake-flagship-login-uri']) {
return null
}

return new URL(twakeConfiguration['twake-flagship-login-uri'])
} else {
return null
}
} catch {
return null
}
}

export const getLoginUri = async (
companyEmail: string
): Promise<URL | null> => {
try {
const domain = extractDomain(companyEmail)

if (!domain) {
throw new Error()
}

const uriFromWellKnown = await fetchLoginUriWithWellKnown(domain)

if (uriFromWellKnown) {
uriFromWellKnown.searchParams.append(
'redirect_after_oidc',
strings.COZY_SCHEME
)
return uriFromWellKnown
}

return null
} catch {
return null
}
}
Loading