-
Notifications
You must be signed in to change notification settings - Fork 6
RU-T48 Adding SSO login support #231
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| // Mock for expo-auth-session | ||
| const mockExchangeCodeAsync = jest.fn(); | ||
| const mockMakeRedirectUri = jest.fn(() => 'resgridunit://auth/callback'); | ||
| const mockUseAutoDiscovery = jest.fn(() => ({ | ||
| authorizationEndpoint: 'https://idp.example.com/authorize', | ||
| tokenEndpoint: 'https://idp.example.com/token', | ||
| })); | ||
| const mockUseAuthRequest = jest.fn(() => [ | ||
| { codeVerifier: 'test-verifier' }, | ||
| null, | ||
| jest.fn(), | ||
| ]); | ||
|
|
||
| module.exports = { | ||
| makeRedirectUri: mockMakeRedirectUri, | ||
| useAutoDiscovery: mockUseAutoDiscovery, | ||
| useAuthRequest: mockUseAuthRequest, | ||
| exchangeCodeAsync: mockExchangeCodeAsync, | ||
| ResponseType: { Code: 'code' }, | ||
| __esModule: true, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| // Mock for expo-web-browser | ||
| const maybeCompleteAuthSession = jest.fn(() => ({ type: 'success' })); | ||
| const openBrowserAsync = jest.fn(() => Promise.resolve({ type: 'dismiss' })); | ||
| const openAuthSessionAsync = jest.fn(() => Promise.resolve({ type: 'dismiss' })); | ||
| const dismissBrowser = jest.fn(); | ||
|
|
||
| module.exports = { | ||
| maybeCompleteAuthSession, | ||
| openBrowserAsync, | ||
| openAuthSessionAsync, | ||
| dismissBrowser, | ||
| __esModule: true, | ||
| }; |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,5 +1,5 @@ | ||||||
| import { zodResolver } from '@hookform/resolvers/zod'; | ||||||
| import { AlertTriangle, EyeIcon, EyeOffIcon } from 'lucide-react-native'; | ||||||
| import { AlertTriangle, EyeIcon, EyeOffIcon, LogIn, ShieldCheck } from 'lucide-react-native'; | ||||||
| import { useColorScheme } from 'nativewind'; | ||||||
| import React, { useState } from 'react'; | ||||||
| import type { SubmitHandler } from 'react-hook-form'; | ||||||
|
|
@@ -28,7 +28,7 @@ | |||||
| .string({ | ||||||
| required_error: 'Password is required', | ||||||
| }) | ||||||
| .min(6, 'Password must be at least 6 characters'), | ||||||
| .min(1, 'Password is required'), | ||||||
| }); | ||||||
|
|
||||||
| const loginFormSchema = createLoginFormSchema(); | ||||||
|
|
@@ -40,14 +40,17 @@ | |||||
| isLoading?: boolean; | ||||||
| error?: string; | ||||||
| onServerUrlPress?: () => void; | ||||||
| /** Called when the user taps "Sign In with SSO" to navigate to the SSO login page */ | ||||||
| onSsoPress?: () => void; | ||||||
| }; | ||||||
|
|
||||||
| export const LoginForm = ({ onSubmit = () => {}, isLoading = false, error = undefined, onServerUrlPress }: LoginFormProps) => { | ||||||
| export const LoginForm = ({ onSubmit = () => { }, isLoading = false, error = undefined, onServerUrlPress, onSsoPress }: LoginFormProps) => { | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix formatting: extra whitespace. Static analysis flagged extra whitespace in the function signature. -export const LoginForm = ({ onSubmit = () => { }, isLoading = false, error = undefined, onServerUrlPress, onSsoPress }: LoginFormProps) => {
+export const LoginForm = ({ onSubmit = () => {}, isLoading = false, error = undefined, onServerUrlPress, onSsoPress }: LoginFormProps) => {📝 Committable suggestion
Suggested change
🧰 Tools🪛 GitHub Check: test[warning] 47-47: 🤖 Prompt for AI Agents |
||||||
| const { colorScheme } = useColorScheme(); | ||||||
| const { t } = useTranslation(); | ||||||
| const { | ||||||
| control, | ||||||
| handleSubmit, | ||||||
| getValues, | ||||||
| formState: { errors }, | ||||||
| } = useForm<FormType>({ | ||||||
| resolver: zodResolver(loginFormSchema), | ||||||
|
|
@@ -60,9 +63,7 @@ | |||||
| const [showPassword, setShowPassword] = useState(false); | ||||||
|
|
||||||
| const handleState = () => { | ||||||
| setShowPassword((showState) => { | ||||||
| return !showState; | ||||||
| }); | ||||||
| setShowPassword((showState) => !showState); | ||||||
| }; | ||||||
| const handleKeyPress = () => { | ||||||
| Keyboard.dismiss(); | ||||||
|
|
@@ -74,12 +75,11 @@ | |||||
| <View className="flex-1 justify-center p-4"> | ||||||
| <View className="items-center justify-center"> | ||||||
| <Image style={{ width: '96%' }} source={colorScheme === 'dark' ? require('@assets/images/Resgrid_JustText_White.png') : require('@assets/images/Resgrid_JustText.png')} resizeMode="contain" /> | ||||||
| <Text className="pb-6 text-center text-4xl font-bold">Sign In</Text> | ||||||
|
|
||||||
| <Text className="mb-6 max-w-xl text-center text-gray-500"> | ||||||
| To login in to the Resgrid Unit app, please enter your username and password. Resgrid Unit is an app designed to interface between a Unit (apparatus, team, etc) and the Resgrid system. | ||||||
| </Text> | ||||||
| <Text className="pb-6 text-center text-4xl font-bold">{t('login.title')}</Text> | ||||||
| <Text className="mb-6 max-w-xl text-center text-gray-500">{t('login.subtitle')}</Text> | ||||||
| </View> | ||||||
|
|
||||||
| {/* Username */} | ||||||
| <FormControl isInvalid={!!errors?.username || !validated.usernameValid} className="w-full"> | ||||||
| <FormControlLabel> | ||||||
| <FormControlLabelText>{t('login.username')}</FormControlLabelText> | ||||||
|
|
@@ -91,10 +91,10 @@ | |||||
| rules={{ | ||||||
| validate: async (value) => { | ||||||
| try { | ||||||
| await loginFormSchema.parseAsync({ username: value }); | ||||||
| await loginFormSchema.parseAsync({ username: value, password: 'placeholder' }); | ||||||
| return true; | ||||||
| } catch (error: any) { | ||||||
| return error.message; | ||||||
| } catch (err: any) { | ||||||
| return err.message; | ||||||
| } | ||||||
| }, | ||||||
| }} | ||||||
|
|
@@ -106,7 +106,7 @@ | |||||
| onChangeText={onChange} | ||||||
| onBlur={onBlur} | ||||||
| onSubmitEditing={handleKeyPress} | ||||||
| returnKeyType="done" | ||||||
| returnKeyType="next" | ||||||
| autoCapitalize="none" | ||||||
| autoComplete="off" | ||||||
| /> | ||||||
|
|
@@ -115,10 +115,11 @@ | |||||
| /> | ||||||
| <FormControlError> | ||||||
| <FormControlErrorIcon as={AlertTriangle} className="text-red-500" /> | ||||||
| <FormControlErrorText className="text-red-500">{errors?.username?.message || (!validated.usernameValid && 'Username not found')}</FormControlErrorText> | ||||||
| <FormControlErrorText className="text-red-500">{errors?.username?.message}</FormControlErrorText> | ||||||
| </FormControlError> | ||||||
| </FormControl> | ||||||
| {/* Label Message */} | ||||||
|
|
||||||
| {/* Password form */} | ||||||
| <FormControl isInvalid={!!errors.password || !validated.passwordValid} className="w-full"> | ||||||
| <FormControlLabel> | ||||||
| <FormControlLabelText>{t('login.password')}</FormControlLabelText> | ||||||
|
|
@@ -130,10 +131,10 @@ | |||||
| rules={{ | ||||||
| validate: async (value) => { | ||||||
| try { | ||||||
| await loginFormSchema.parseAsync({ password: value }); | ||||||
| await loginFormSchema.parseAsync({ username: getValues('username'), password: value }); | ||||||
| return true; | ||||||
| } catch (error: any) { | ||||||
| return error.message; | ||||||
| } catch (err: any) { | ||||||
| return err.message; | ||||||
| } | ||||||
| }, | ||||||
| }} | ||||||
|
|
@@ -168,16 +169,27 @@ | |||||
| <ButtonText className="ml-2 text-sm font-medium">{t('login.login_button_loading')}</ButtonText> | ||||||
| </Button> | ||||||
| ) : ( | ||||||
| <Button className="mt-8 w-full" variant="solid" action="primary" onPress={handleSubmit(onSubmit)}> | ||||||
| <ButtonText>Log in</ButtonText> | ||||||
| <Button className="mt-8 w-full" variant="solid" action="primary" onPress={handleSubmit(onSubmit)} accessibilityLabel={t('login.login_button')}> | ||||||
| <ButtonText>{t('login.login_button')}</ButtonText> | ||||||
| </Button> | ||||||
| )} | ||||||
|
|
||||||
| {onServerUrlPress && ( | ||||||
| <Button className="mt-14 w-full" variant="outline" action="secondary" onPress={onServerUrlPress}> | ||||||
| <ButtonText>{t('settings.server_url')}</ButtonText> | ||||||
| </Button> | ||||||
| )} | ||||||
| {error ? <Text className="mt-4 text-center text-sm text-red-500">{error}</Text> : null} | ||||||
|
|
||||||
| {/* Server URL + Sign In with SSO — side by side small buttons */} | ||||||
| <View className="mt-6 flex-row gap-x-2"> | ||||||
| {onServerUrlPress ? ( | ||||||
| <Button className="flex-1" variant="outline" action="secondary" size="sm" onPress={onServerUrlPress}> | ||||||
| <ButtonText className="text-xs">{t('settings.server_url')}</ButtonText> | ||||||
| </Button> | ||||||
| ) : null} | ||||||
| {onSsoPress ? ( | ||||||
| <Button className="flex-1" variant="outline" action="secondary" size="sm" onPress={onSsoPress}> | ||||||
| <ShieldCheck size={14} style={{ marginRight: 4 }} /> | ||||||
| <ButtonText className="text-xs">{t('login.sso_button')}</ButtonText> | ||||||
| </Button> | ||||||
| ) : null} | ||||||
| </View> | ||||||
| </View> | ||||||
| </KeyboardAvoidingView> | ||||||
| ); | ||||||
|
|
||||||
Uh oh!
There was an error while loading. Please reload this page.