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
2 changes: 1 addition & 1 deletion front/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@
"experiments": {
"tsconfigPaths": true
},
"plugins": ["expo-localization"]
"plugins": ["expo-localization", "expo-font", "expo-secure-store"]
}
}
7 changes: 7 additions & 0 deletions front/assets/translations/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,12 @@
"delete": "Delete availability",
"deleteConfirm": "Are you sure you want to delete this availability?"
}
},
"login": {
"login": "Sign in",
"logout": "Sign out"
},
"app": {
"branding": "LinkedOut"
}
}
7 changes: 7 additions & 0 deletions front/assets/translations/fr_FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,12 @@
"delete": "Supprimer la disponibilité",
"deleteConfirm": "Voulez-vous vraiment supprimer cette disponibilité?"
}
},
"login": {
"login": "Se connecter",
"logout": "Se déconnecter"
},
"app": {
"branding": "LinkedOut"
}
}
3,809 changes: 1,619 additions & 2,190 deletions front/package-lock.json

Large diffs are not rendered by default.

31 changes: 16 additions & 15 deletions front/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,30 +20,31 @@
"@react-navigation/native-stack": "^6.9.16",
"@reduxjs/toolkit": "^1.9.7",
"@types/color": "^3.0.5",
"@types/react": "^18.2.33",
"@types/react": "~18.2.45",
"color": "^4.2.3",
"deepmerge": "^4.3.1",
"expo": "^49.0.16",
"expo-auth-session": "~5.0.2",
"expo-crypto": "~12.4.1",
"expo-font": "~11.4.0",
"expo-localization": "~14.3.0",
"expo-splash-screen": "~0.20.5",
"expo-status-bar": "~1.6.0",
"expo-system-ui": "~2.4.0",
"expo-web-browser": "~12.3.2",
"expo": "^50.0.2",
"expo-auth-session": "~5.4.0",
"expo-crypto": "~12.8.0",
"expo-font": "~11.10.2",
"expo-localization": "~14.8.3",
"expo-splash-screen": "~0.26.3",
"expo-status-bar": "~1.11.1",
"expo-system-ui": "~2.9.3",
"expo-web-browser": "~12.8.1",
"i18n-js": "^4.3.2",
"react": "^18.2.0",
"react-native": "0.72.6",
"react-native-pager-view": "6.2.0",
"react-native": "0.73.2",
"react-native-pager-view": "6.2.3",
"react-native-paper": "^5.11.1",
"react-native-paper-dates": "^0.20.3",
"react-native-paper-dropdown": "^1.0.7",
"react-native-safe-area-context": "4.6.3",
"react-native-screens": "~3.22.0",
"react-native-safe-area-context": "4.8.2",
"react-native-screens": "~3.29.0",
"react-native-tab-view": "^3.5.2",
"react-redux": "^8.1.3",
"typescript": "^5.2.2"
"typescript": "^5.3.0",
"expo-secure-store": "~12.8.1"
},
"devDependencies": {
"@babel/core": "^7.23.2",
Expand Down
43 changes: 5 additions & 38 deletions front/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
import { NavigationContainer } from '@react-navigation/native';
import { ApiProvider } from '@reduxjs/toolkit/query/react';
import { useFonts } from 'expo-font';
import * as SplashScreen from 'expo-splash-screen';
import { StatusBar } from 'expo-status-bar';
import { useCallback } from 'react';
import { useColorScheme } from 'react-native';
import { PaperProvider } from 'react-native-paper';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { Provider } from 'react-redux';

import RootNavigator from '@/RootNavigator';
import { apiSlice } from '@/store/api/apiSlice';
import SplashHandler from '@/SplashHandler';
import { store } from '@/store/store';
import { DarkTheme, LightTheme } from '@/utils/theme';

// We want to hide the splash screen ourselves
SplashScreen.preventAutoHideAsync();

/**
* The root component for this application.
* @constructor
Expand All @@ -25,36 +16,12 @@ const App = () => {
const colorScheme = useColorScheme();
const theme = colorScheme === 'dark' ? DarkTheme : LightTheme;

// Load application fonts
const [fontsLoaded, fontError] = useFonts({
RedHatDisplay: require('$/fonts/RedHatDisplay.ttf'),
});

// Hide the splash screen when everything is initialized
const shouldHideSplashScreen = fontsLoaded || !!fontError;

const handleNavigationContainerReady = useCallback(async () => {
if (shouldHideSplashScreen) {
await SplashScreen.hideAsync();
}
}, [shouldHideSplashScreen]);

if (!shouldHideSplashScreen) {
return null;
}

return (
<Provider store={store}>
<PaperProvider theme={theme}>
<ApiProvider api={apiSlice}>
<NavigationContainer
theme={theme}
onReady={handleNavigationContainerReady}
>
<StatusBar style='auto' />
<RootNavigator />
</NavigationContainer>
</ApiProvider>
<SafeAreaProvider>
<SplashHandler />
</SafeAreaProvider>
</PaperProvider>
</Provider>
);
Expand Down
19 changes: 13 additions & 6 deletions front/src/RootNavigator.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { createNativeStackNavigator } from '@react-navigation/native-stack';

import InternalTabNav from '@/pages/InternalTabNav';
import MainTabNav from '@/pages/MainTabNav';
import LoginNav from '@/pages/LoginNav';
import { useAppSelector } from '@/store/hooks';

/**
* The parameter list for the RootStack navigator.
*/
export type RootStackParamList = {
Internal: undefined;
Login: undefined;
Main: undefined;
};

Expand All @@ -18,13 +20,18 @@ const RootStack = createNativeStackNavigator<RootStackParamList>();
* @constructor
*/
const RootNavigator = () => {
// Store hooks
const authState = useAppSelector((state) => state.auth.state);

return (
<RootStack.Navigator
initialRouteName='Internal'
screenOptions={{ headerShown: false }}
>
<RootStack.Navigator screenOptions={{ headerShown: false }}>
{authState === 'authenticated' ? (
<RootStack.Screen name='Main' component={InternalTabNav} />
) : (
<RootStack.Screen name='Login' component={LoginNav} />
)}

<RootStack.Screen name='Internal' component={InternalTabNav} />
<RootStack.Screen name='Main' component={MainTabNav} />
</RootStack.Navigator>
);
};
Expand Down
61 changes: 61 additions & 0 deletions front/src/SplashHandler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { NavigationContainer } from '@react-navigation/native';
import { useFonts } from 'expo-font';
import * as SplashScreen from 'expo-splash-screen';
import { StatusBar } from 'expo-status-bar';
import { useCallback, useEffect } from 'react';
import { useColorScheme } from 'react-native';

import RootNavigator from '@/RootNavigator';
import { init } from '@/store/features/authenticationSlice';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { DarkTheme, LightTheme } from '@/utils/theme';

// We want to hide the splash screen ourselves
SplashScreen.preventAutoHideAsync();

/**
* Handles hiding the splash screen when the application is ready.
* @constructor
*/
const SplashHandler = () => {
// Application theme
const colorScheme = useColorScheme();
const theme = colorScheme === 'dark' ? DarkTheme : LightTheme;

// Store hooks
const authState = useAppSelector((state) => state.auth.state);
const dispatch = useAppDispatch();

// Load application fonts
const [fontsLoaded, fontError] = useFonts({
RedHatDisplay: require('$/fonts/RedHatDisplay.ttf'),
});

// Init the authentication store slice
useEffect(() => {
dispatch(init());
}, [dispatch]);

// Hide the splash screen when everything is initialized
const shouldHideSplashScreen =
(fontsLoaded || !!fontError) && authState !== 'unknown';

const handleNavigationContainerReady = useCallback(async () => {
if (shouldHideSplashScreen) {
await SplashScreen.hideAsync();
}
}, [shouldHideSplashScreen]);

if (!shouldHideSplashScreen) {
return null;
}

return (
<NavigationContainer theme={theme} onReady={handleNavigationContainerReady}>
<StatusBar style='auto' />
<RootNavigator />
</NavigationContainer>
);
};

export default SplashHandler;
82 changes: 82 additions & 0 deletions front/src/components/login/LoginButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {
ResponseType,
makeRedirectUri,
useAuthRequest,
useAutoDiscovery,
} from 'expo-auth-session';
import * as AuthSession from 'expo-auth-session';
import * as WebBrowser from 'expo-web-browser';
import { useCallback } from 'react';
import { Button } from 'react-native-paper';

import { login } from '@/store/features/authenticationSlice';
import { useAppDispatch } from '@/store/hooks';
import i18n from '@/utils/i18n';

WebBrowser.maybeCompleteAuthSession();

/**
* The button for logging in to the app.
* @constructor
*/
const LoginButton = () => {
// Store hooks
const dispatch = useAppDispatch();

// OIDC settings
const oidcDiscoveryUrl = process.env.EXPO_PUBLIC_OIDC_DISCOVERY_URL;
const oidcClientId = process.env.EXPO_PUBLIC_OIDC_CLIENT_ID;

// OIDC discovery
const discovery = useAutoDiscovery(oidcDiscoveryUrl);

// OIDC auth request
const [request, , promptAsync] = useAuthRequest(
{
clientId: oidcClientId,
redirectUri: makeRedirectUri({
scheme: 'linkedout',
}),
responseType: ResponseType.Code,
scopes: ['openid', 'profile', 'email'],
usePKCE: true,
},
discovery,
);

const handleLoginPress = useCallback(async () => {
// Open the web view to the IdP login page
const authResult = await promptAsync();

if (authResult.type !== 'success') {
console.error('Auth failed');
return;
}

try {
// Exchange the auth code for an access token
const tokenResponse = await AuthSession.exchangeCodeAsync(
{
clientId: request.clientId,
code: authResult.params.code,
extraParams: { code_verifier: request.codeVerifier || '' },
redirectUri: request.redirectUri,
},
discovery,
);

// Dispatch the access token to the store
dispatch(login(tokenResponse.accessToken));
} catch (e) {
console.error(e);
}
}, [discovery, dispatch, promptAsync, request]);

return (
<Button mode='contained' onPress={handleLoginPress}>
{i18n.t('login.login')}
</Button>
);
};

export default LoginButton;
60 changes: 60 additions & 0 deletions front/src/components/login/LogoutButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import * as AuthSession from 'expo-auth-session';
import { TokenTypeHint, useAutoDiscovery } from 'expo-auth-session';
import { useCallback } from 'react';
import { Button } from 'react-native-paper';

import { logout } from '@/store/features/authenticationSlice';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import i18n from '@/utils/i18n';

/**
* The button for logging out of the app.
* @constructor
*/
const LogoutButton = () => {
// Store hooks
const authState = useAppSelector((state) => state.auth);
const dispatch = useAppDispatch();

// OIDC settings
const oidcDiscoveryUrl = process.env.EXPO_PUBLIC_OIDC_DISCOVERY_URL;
const oidcClientId = process.env.EXPO_PUBLIC_OIDC_CLIENT_ID;

// OIDC discovery
const discovery = useAutoDiscovery(oidcDiscoveryUrl);

const handleLogoutPress = useCallback(async () => {
if (authState.state !== 'authenticated') {
return;
}

const token = authState.token;

try {
// Revoke the access token
const res = await AuthSession.revokeAsync(
{
clientId: oidcClientId,
token,
tokenTypeHint: TokenTypeHint.AccessToken,
},
discovery,
);

// Dispatch the logout action
if (res) {
dispatch(logout());
}
} catch (e) {
console.error(e);
}
}, [authState, discovery, dispatch, oidcClientId]);

return (
<Button mode='contained' onPress={handleLogoutPress}>
{i18n.t('login.logout')}
</Button>
);
};

export default LogoutButton;
Loading