@fto-consult/expo-ui
Version:
Bibliothèque de composants UI Expo,react-native
316 lines (308 loc) • 12.9 kB
JavaScript
import React from "$react"
import "react-native-gesture-handler";
import { AppState} from "react-native";
import BackHandler from "$ecomponents/BackHandler";
import * as Linking from 'expo-linking';
import APP from "$capp";
import {AppStateService,trackIDLE,stop as stopIDLE} from "$capp/idle";
import {Provider as DrawerProvider} from "$ecomponents/Drawer";
import { NavigationContainer} from '@react-navigation/native';
import {navigationRef} from "$cnavigation"
import NetInfo from '$cutils/NetInfo';
import Auth from "$cauth";
import {isNativeMobile,isElectron} from "$cplatform";
import Navigation from "../navigation";
import {set as setSession,get as getSession} from "$session";
import { showConfirm } from "$ecomponents/Dialog";
import {close as closePreloader, isVisible as isPreloaderVisible} from "$epreloader";
import SplashScreen from "$ecomponents/SplashScreen";
import {useAppComponent} from "$econtext/hooks";
import {decycle} from "$cutils/json";
import { setIsInitialized} from "$capp/utils";
import {isObj,defaultObj,defaultStr} from "$cutils";
import {loadFonts} from "$ecomponents/Icon/Font";
import appConfig from "$capp/config";
import Preloader from "$preloader";
import {PreloaderProvider} from "$epreloader";
import BottomSheetProvider from "$ecomponents/BottomSheet/Provider";
import DialogProvider from "$ecomponents/Dialog/Provider";
import SimpleSelect from '$ecomponents/SimpleSelect';
import {Provider as AlertProvider} from '$ecomponents/Dialog/confirm/Alert';
import { DialogProvider as FormDataDialogProvider } from '$eform/FormData';
import ErrorBoundaryProvider from "$ecomponents/ErrorBoundary/Provider";
import notify, {notificationRef} from "$notify";
import DropdownAlert from '$ecomponents/Dialog/DropdownAlert';
import { PreferencesContext } from '../Preferences';
import ErrorBoundary from "$ecomponents/ErrorBoundary";
import mainTheme, {updateTheme,defaultTheme} from "$theme";
import StatusBar from "$ecomponents/StatusBar";
import {Provider as PaperProvider,Portal } from 'react-native-paper';
import FontIcon from "$ecomponents/Icon/Font";
import useContext from "$econtext/hooks";
import { StyleSheet } from "react-native";
import Logo from "$ecomponents/Logo";
import AppEntryRootView from "./RootView";
import View from "$ecomponents/View";
import { SafeAreaProvider } from 'react-native-safe-area-context';
let MAX_BACK_COUNT = 1;
let countBack = 0;
let isBackConfirmShowing = false;
const resetExitCounter = ()=>{
countBack = 0
isBackConfirmShowing = false;
};
const NAVIGATION_PERSISTENCE_KEY = 'NAVIGATION_STATE';
/****
* init {function}: ()=>Promise<{}> est la fonction d'initialisation de l'application
* initialRouteName : la route initiale par défaut
* getStartedRouteName : la route par défaut de getStarted lorsque l'application est en mode getStarted, c'est à dire lorsque la fonction init renvoie une erreur (reject)
*/
function App({init:initApp,initialRouteName:appInitialRouteName,children}) {
AppStateService.init();
const SplashScreenComponent = useAppComponent("SplashScreen");
const {FontsIconsFilter,beforeExit,preferences:appPreferences,navigation,getStartedRouteName,withSplashScreen,components:{MainProvider}} = useContext();
const {containerProps} = navigation;
const [initialState, setInitialState] = React.useState(undefined);
const appReadyRef = React.useRef(true);
const [state,setState] = React.useState({
isLoading : true,
isInitialized:false,
hasCallInitApp : false,
});
React.useEffect(() => {
const loadResources = ()=>{
return new Promise((resolve)=>{
loadFonts(FontsIconsFilter).catch((e)=>{
console.warn(e," ierror loading app resources fonts");
}).finally(()=>{
resolve(true);
});
})
}
const restoreState = () => {
return new Promise((resolve,reject)=>{
(async ()=>{
try {
const initialUrl = await Linking.getInitialURL();
if (isNativeMobile() || initialUrl === null) {
const savedState = getSession(NAVIGATION_PERSISTENCE_KEY);
if (isObj(savedState)) {
setInitialState(savedState);
}
}
} catch(e){ console.log(e," is state error")}
finally {
appReadyRef.current = true;
resolve({});
}
})();
});
};
const subscription = AppState.addEventListener('change', AppStateService.getInstance().handleAppStateChange);
const beforeExitApp = (cb)=>{
return new Promise((resolve,reject)=>{
Preloader.closeAll();
showConfirm({
title : "Quitter l'application",
message : 'Voulez vous vraiment quitter l\'application?',
yes : 'Oui',
no : 'Non',
onSuccess : ()=>{
const foreceExit = ()=>{
BackHandler.exitApp();
if(isElectron() && window.ELECTRON && typeof ELECTRON.exitApp =='function'){
ELECTRON.exitApp({APP});
}
}
const exit = ()=>{
if(typeof beforeExit =='function'){
Promise.resolve(beforeExit()).then(foreceExit).catch(reject);
}
foreceExit();
}
const r = {APP,exit};
APP.trigger(APP.EVENTS.BEFORE_EXIT,exit,(result)=>{
if(isObj(result) || Array.isArray(result)){
for(let ik in result){
if(result[ik] === false) return reject({message:'EXIT APP DENIED BY BEFORE EXIT EVENT HANDLER AT POSITON {0}'.sprintf(ik)});
}
}
resolve(r);
if(typeof cb =='function'){
cb(r);
}
});
},
onCancel : reject
})
})
}
/**** onBeforeExit prend en paramètre la fonction de rappel CB, qui lorsque la demande de sortie d'application est acceptée, alors elle est exécutée */
if(typeof APP.beforeExit !=='function'){
Object.defineProperties(APP,{
beforeExit : {
value : beforeExitApp,
}
})
}
const backAction = (args) => {
if(navigationRef && navigationRef.canGoBack()? true : false){
resetExitCounter();
navigationRef.goBack(null);
return false;
}
if(isBackConfirmShowing) {
return;
}
if(countBack < MAX_BACK_COUNT){
countBack++;
isBackConfirmShowing = false;
if(countBack === MAX_BACK_COUNT){
notify.toast({text:'Cliquez à nouveau pour quiiter l\'application'});
}
if(countBack === 2 && isPreloaderVisible()) {
closePreloader();
}
return false;
}
isBackConfirmShowing = true;
return beforeExitApp().finally(x=>{
isBackConfirmShowing = false;
}).then(({exit})=>{
exit();
})
};
const unsubscribeNetInfo = NetInfo.addEventListener(state => {
APP.setOnlineState(state);
});
NetInfo.fetch().catch((e)=>{
console.log(e," is net info")
});
loadResources().finally(()=>{
Promise.resolve((typeof initApp =='function'?initApp : x=>true)({appConfig,contex:{setState}})).then((args)=>{
if(Auth.isLoggedIn()){
Auth.updateTheme(Auth.getLoggedUser());
}
setState({
...state,hasGetStarted:true,...defaultObj(args && args?.state),hasCallInitApp:true,isInitialized:true,isLoading : false,
});
}).catch((e)=>{
console.error(e," loading resources for app initialization");
setState({...state,isInitialized:true,hasCallInitApp:true,isLoading : false,hasGetStarted:false});
})
});
const Events = {}
let events = [];
if(navigationRef && navigationRef.addListener){
for(let i in Events){
events.push(navigationRef.addListener(i,Events[i]));
}
}
APP.onElectron("BEFORE_EXIT",()=>{
return beforeExitApp().then(({exit})=>{
exit();
})
});
APP.on(APP.EVENTS.BACK_BUTTON,backAction);
return () => {
APP.off(APP.EVENTS.BACK_BUTTON,backAction);
if(subscription && subscription.remove){
subscription.remove();
}
events.map((ev)=>{
if(typeof ev =="function") ev();
})
unsubscribeNetInfo();
stopIDLE(false,true);
}
}, []);
const {isInitialized} = state;
const isLoading = state.isLoading || !isInitialized || !appReadyRef.current? true : false;
React.useEffect(()=>{
if(isInitialized){
setIsInitialized(true);
trackIDLE(true);
}
},[isInitialized]);
const hasGetStarted = state.hasGetStarted !== false? true : false;
const themeRef = React.useRef(null);
const [theme,setTheme] = React.useState(themeRef.current || updateTheme(defaultTheme));
themeRef.current = theme;
const updatePreferenceTheme = (customTheme,persist)=>{
setTheme(updateTheme(customTheme));
};
const forceRender = React.useForceRender();
const pref = typeof appPreferences =='function'? appPreferences({setTheme,forceRender,updateTheme:updatePreferenceTheme}) : appPreferences;
const preferences = React.useMemo(()=>({
updateTheme:updatePreferenceTheme,
theme,
...defaultObj(pref),
}),[theme,pref]);
const isLoaded = !isLoading && state.hasCallInitApp;
const child = isLoaded ? <NavigationContainer
ref={navigationRef}
initialState={initialState}
{...containerProps}
onStateChange={(state,...rest) =>{
//setSession(NAVIGATION_PERSISTENCE_KEY,decycle(state),false);
if(typeof containerProps.onStateChange =='function'){
containerProps.onStateChange(state,...rest);
}
}}
fallback = {React.isValidElement(containerProps.fallback) ? containerProps.fallback : <View style={[mainTheme.styles.flex1,mainTheme.styles.justifyContentCenter,mainTheme.styles.alignItemsCenter]}>
<Logo.Progress/>
</View>}
>
<Navigation
initialRouteName = {defaultStr(hasGetStarted ? appInitialRouteName : getStartedRouteName,"Home")}
state = {state}
hasGetStarted = {hasGetStarted}
isInitialized = {isInitialized}
onGetStart = {(e)=>{
setState({...state,hasGetStarted:true})
}}
/>
</NavigationContainer> : null;
const content = isLoaded ? typeof children == 'function'? children({children:child,appConfig,config:appConfig}) : child : null;
const myChildren = <PreferencesContext.Provider value={preferences}>
{isLoaded ? React.isValidElement(content) && content || child : null}
</PreferencesContext.Provider>;
return <SafeAreaProvider>
<AppEntryRootView MainProvider={MainProvider} isInitialized={state.hasCallInitApp} isLoading={isLoading} hasGetStarted={hasGetStarted} isLoaded={isLoaded}>
<PaperProvider
theme={theme}
settings={{
icon: (props) => {
return <FontIcon {...props}/>
},
}}
>
<Portal.Host testID="RN_NativePaperPortalHost">
<ErrorBoundaryProvider/>
<PreloaderProvider/>
<DialogProvider responsive testID={"RN_MainAppDialogProvider"}/>
<AlertProvider SimpleSelect={SimpleSelect}/>
<FormDataDialogProvider/>
<BottomSheetProvider/>
<DropdownAlert ref={notificationRef}/>
<ErrorBoundary>
<StatusBar/>
{withSplashScreen !== false || false ? <SplashScreen
children = {myChildren}
isLoaded = {isLoaded}
Component = {SplashScreenComponent}
/> : myChildren}
</ErrorBoundary>
</Portal.Host>
<DrawerProvider testID="RN_DrawerProviderRight"/>
</PaperProvider>
</AppEntryRootView>
</SafeAreaProvider>
}
export default App;
const styles = StyleSheet.create({
gesture : {
flex : 1,
flexGrow : 1,
}
});