@fto-consult/expo-ui 6.67.15 → 6.68.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fto-consult/expo-ui",
3
- "version": "6.67.15",
3
+ "version": "6.68.0",
4
4
  "description": "Bibliothèque de composants UI Expo,react-native",
5
5
  "main": "main",
6
6
  "scripts": {
@@ -71,7 +71,7 @@
71
71
  "@expo/html-elements": "^0.5.1",
72
72
  "@expo/vector-icons": "^13.0.0",
73
73
  "@faker-js/faker": "^8.0.2",
74
- "@fto-consult/common": "^3.58.0",
74
+ "@fto-consult/common": "^3.58.1",
75
75
  "@pchmn/expo-material3-theme": "^1.3.1",
76
76
  "@react-native-async-storage/async-storage": "1.18.2",
77
77
  "@react-native-community/datetimepicker": "7.2.0",
@@ -1,174 +1,334 @@
1
- import '$session';
2
- import React from 'react';
3
- import {SWRConfig} from "$swr";
4
- import App from './App';
5
- import notify from "$notify";
6
- import APP from "$app";
7
- import {isMobileNative} from "$cplatform";
8
- import {setDeviceIdRef} from "$capp";
9
- import {showPrompt} from "$ecomponents/Dialog/confirm";
10
- import { AppState } from 'react-native'
11
- import {canFetchOffline} from "$capi/utils";
12
- import {defaultNumber} from "$cutils";
13
- import { timeout as SWR_REFRESH_TIMEOUT} from '$ecomponents/Datagrid/SWRDatagrid';
1
+ import React from "$react"
2
+ import { AppState} from "react-native";
3
+ import BackHandler from "$ecomponents/BackHandler";
4
+ import * as Linking from 'expo-linking';
5
+ import APP from "$capp";
6
+ import {AppStateService,trackIDLE,stop as stopIDLE} from "$capp/idle";
7
+ import { NavigationContainer} from '@react-navigation/native';
8
+ import {navigationRef} from "$cnavigation"
9
+ import NetInfo from '$cutils/NetInfo';
10
+ import Auth from "$cauth";
11
+ import {isNativeMobile,isElectron} from "$cplatform";
12
+ import Navigation from "../navigation";
13
+ import {set as setSession,get as getSession} from "$session";
14
+ import { showConfirm } from "$ecomponents/Dialog";
15
+ import {close as closePreloader, isVisible as isPreloaderVisible} from "$epreloader";
16
+ import SplashScreen from "$ecomponents/SplashScreen";
17
+ import {decycle} from "$cutils/json";
18
+ import init from "$capp/init";
19
+ import { setIsInitialized} from "$capp/utils";
20
+ import {isObj,isNonNullString,isPromise,defaultObj,defaultStr} from "$cutils";
21
+ import {loadFonts} from "$ecomponents/Icon/Font";
22
+ import appConfig from "$capp/config";
23
+ import Preloader from "$preloader";
24
+ import {PreloaderProvider} from "$epreloader";
25
+ import BottomSheetProvider from "$ecomponents/BottomSheet/Provider";
26
+ import DialogProvider from "$ecomponents/Dialog/Provider";
27
+ import SimpleSelect from '$ecomponents/SimpleSelect';
28
+ import {Provider as AlertProvider} from '$ecomponents/Dialog/confirm/Alert';
29
+ import { DialogProvider as FormDataDialogProvider } from '$eform/FormData';
30
+ import ErrorBoundaryProvider from "$ecomponents/ErrorBoundary/Provider";
31
+ import notify, {notificationRef} from "$notify";
32
+ import DropdownAlert from '$ecomponents/Dialog/DropdownAlert';
33
+ import { PreferencesContext } from '../Preferences';
34
+ import ErrorBoundary from "$ecomponents/ErrorBoundary";
35
+ import {updateTheme,defaultTheme} from "$theme";
36
+ import StatusBar from "$ecomponents/StatusBar";
37
+ import {Provider as PaperProvider,Portal } from 'react-native-paper';
38
+ import FontIcon from "$ecomponents/Icon/Font";
39
+ import useContext from "$econtext/hooks";
40
+ import { GestureHandlerRootView } from 'react-native-gesture-handler';
41
+ import { StyleSheet } from "react-native";
42
+ import Logo from "$ecomponents/Logo";
43
+ import AppEntryRootView from "./RootView";
44
+ import { SafeAreaProvider } from 'react-native-safe-area-context';
14
45
  import { Dimensions,Keyboard } from 'react-native';
15
46
  import {isTouchDevice} from "$platform";
16
- import * as Utils from "$cutils";
17
- import {useContext} from "$econtext/hooks";
18
- import appConfig from "$capp/config";
19
- import { useKeepAwake } from 'expo-keep-awake';
20
47
 
21
- Object.map(Utils,(v,i)=>{
22
- if(typeof v =='function' && typeof window !='undefined' && window && !window[i]){
23
- window[i] = v;
24
- }
25
- });
26
48
 
27
- export default function getIndex({render,init}){
28
- const {swrConfig} = useContext();
29
- const isScreenFocusedRef = React.useRef(true);
30
- isMobileNative() && useKeepAwake();
31
- ///garde pour chaque écran sa date de dernière activité
32
- const screensRef = React.useRef({});//la liste des écrans actifs
33
- const activeScreenRef = React.useRef('');
34
- const prevActiveScreenRef = React.useRef('');
35
- const appStateRef = React.useRef({});
36
- const isKeyboardOpenRef = React.useRef(false);
37
- React.useEffect(()=>{
38
- ///la fonction de rappel lorsque le composant est monté
39
- const onScreenFocus = ({sanitizedName})=>{
40
- prevActiveScreenRef.current = activeScreenRef.current;
41
- if(activeScreenRef.current){
42
- screensRef.current[activeScreenRef.current] = null;
43
- }
44
- screensRef.current[sanitizedName] = new Date();
45
- activeScreenRef.current = sanitizedName;
46
- isScreenFocusedRef.current = true;
47
- }, onScreenBlur = ()=>{
48
- isScreenFocusedRef.current = false;
49
- }
50
- APP.on(APP.EVENTS.SCREEN_FOCUS,onScreenFocus);
51
- APP.on(APP.EVENTS.SCREEN_BLUR,onScreenBlur);
52
- const triggerKeyboardToggle = (status)=>{
53
- APP.trigger(APP.EVENTS.KEYBOARD_DID_TOGGLE,{shown:status,status,visible:status,hide : !status});
54
- }
55
- const keyBoardDidShow = ()=>{
56
- APP.trigger(APP.EVENTS.KEYBOARD_DID_SHOW);
57
- triggerKeyboardToggle(true);
58
- },keyBoardDidHide = ()=>{
59
- APP.trigger(APP.EVENTS.KEYBOARD_DID_HIDE);
60
- triggerKeyboardToggle(false);
61
- }
62
- const keyBoardDidShowListener = Keyboard.addListener("keyboardDidShow",keyBoardDidShow);
63
- const keyBoardDidHideListener = Keyboard.addListener("keyboardDidHide",keyBoardDidHide);
64
- const listener = isTouchDevice() && typeof window !=='undefined' && window && window.visualViewport && typeof window.visualViewport.addEventListener =='function'?
65
- () => {
66
- const minKeyboardHeight = 300;
67
- const screen = Dimensions.get("screen");
68
- const newState = screen.height - minKeyboardHeight > window.visualViewport.height
69
- if (isKeyboardOpenRef.current != newState) {
70
- isKeyboardOpenRef.current = newState;
71
- newState ? keyBoardDidShow() : keyBoardDidHide();
49
+ let MAX_BACK_COUNT = 1;
50
+ let countBack = 0;
51
+ let isBackConfirmShowing = false;
52
+
53
+ const resetExitCounter = ()=>{
54
+ countBack = 0
55
+ isBackConfirmShowing = false;
56
+ };
57
+
58
+ const NAVIGATION_PERSISTENCE_KEY = 'NAVIGATION_STATE';
59
+
60
+ /****
61
+ * init {function}: ()=>Promise<{}> est la fonction d'initialisation de l'application
62
+ * initialRouteName : la route initiale par défaut
63
+ * 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)
64
+ */
65
+ function App({init:initApp,initialRouteName:appInitialRouteName,render}) {
66
+ AppStateService.init();
67
+ const {FontsIconsFilter,beforeExit,preferences:appPreferences,navigation,getStartedRouteName} = useContext();
68
+ const {containerProps} = navigation;
69
+ const [initialState, setInitialState] = React.useState(undefined);
70
+ const appReadyRef = React.useRef(true);
71
+ const [state,setState] = React.useState({
72
+ isLoading : true,
73
+ isInitialized:false,
74
+ hasCallInitApp : false,
75
+ });
76
+ React.useEffect(() => {
77
+ ///la fonction de rappel lorsque le composant est monté
78
+ const triggerKeyboardToggle = (status)=>{
79
+ APP.trigger(APP.EVENTS.KEYBOARD_DID_TOGGLE,{shown:status,status,visible:status,hide : !status});
80
+ }
81
+ const keyBoardDidShow = ()=>{
82
+ APP.trigger(APP.EVENTS.KEYBOARD_DID_SHOW);
83
+ triggerKeyboardToggle(true);
84
+ },keyBoardDidHide = ()=>{
85
+ APP.trigger(APP.EVENTS.KEYBOARD_DID_HIDE);
86
+ triggerKeyboardToggle(false);
87
+ }
88
+ const keyBoardDidShowListener = Keyboard.addListener("keyboardDidShow",keyBoardDidShow);
89
+ const keyBoardDidHideListener = Keyboard.addListener("keyboardDidHide",keyBoardDidHide);
90
+ const loadResources = ()=>{
91
+ return new Promise((resolve)=>{
92
+ loadFonts(FontsIconsFilter).catch((e)=>{
93
+ console.warn(e," ierror loading app resources fonts");
94
+ }).finally(()=>{
95
+ resolve(true);
96
+ });
97
+ })
98
+ }
99
+ const restoreState = () => {
100
+ return new Promise((resolve,reject)=>{
101
+ (async ()=>{
102
+ try {
103
+ const initialUrl = await Linking.getInitialURL();
104
+ if (isNativeMobile() || initialUrl === null) {
105
+ const savedState = getSession(NAVIGATION_PERSISTENCE_KEY);
106
+ if (isObj(savedState)) {
107
+ setInitialState(savedState);
108
+ }
109
+ }
110
+ } catch(e){ console.log(e," is state error")}
111
+ finally {
112
+ appReadyRef.current = true;
113
+ resolve({});
72
114
  }
73
- } : undefined;
74
- if(listener){
75
- window.visualViewport.addEventListener('resize', listener);
76
- }
77
- return ()=>{
78
- APP.off(APP.EVENTS.SCREEN_FOCUS,onScreenFocus);
79
- APP.off(APP.EVENTS.SCREEN_BLUR,onScreenBlur);
80
- keyBoardDidShowListener && keyBoardDidShowListener.remove && keyBoardDidShowListener.remove();
81
- keyBoardDidHideListener && keyBoardDidHideListener.remove && keyBoardDidHideListener.remove();
82
- if(listener){
83
- window.visualViewport.removeEventListener('resize', listener);
115
+ })();
116
+ });
117
+ };
118
+ const subscription = AppState.addEventListener('change', AppStateService.getInstance().handleAppStateChange);
119
+ const beforeExitApp = (cb)=>{
120
+ return new Promise((resolve,reject)=>{
121
+ Preloader.closeAll();
122
+ showConfirm({
123
+ title : "Quitter l'application",
124
+ message : 'Voulez vous vraiment quitter l\'application?',
125
+ yes : 'Oui',
126
+ no : 'Non',
127
+ onSuccess : ()=>{
128
+ const foreceExit = ()=>{
129
+ BackHandler.exitApp();
130
+ if(isElectron() && window.ELECTRON && typeof ELECTRON.exitApp =='function'){
131
+ ELECTRON.exitApp({APP});
132
+ }
133
+ }
134
+ const exit = ()=>{
135
+ if(typeof beforeExit =='function'){
136
+ const r2 = beforeExit()
137
+ if(!isPromise(r2)){
138
+ throw {message:'La fonction before exit du contexte doit retourner une promesse',returnedResult:r2}
139
+ }
140
+ return r2.then(foreceExit).catch(reject);
141
+ }
142
+ foreceExit();
143
+ }
144
+ const r = {APP,exit};
145
+ APP.trigger(APP.EVENTS.BEFORE_EXIT,exit,(result)=>{
146
+ if(isObj(result) || Array.isArray(result)){
147
+ for(let ik in result){
148
+ if(result[ik] === false) return reject({message:'EXIT APP DENIED BY BEFORE EXIT EVENT HANDLER AT POSITON {0}'.sprintf(ik)});
149
+ }
150
+ }
151
+ resolve(r);
152
+ if(typeof cb =='function'){
153
+ cb(r);
154
+ }
155
+ });
156
+ },
157
+ onCancel : reject
158
+ })
159
+ })
160
+ }
161
+ /**** 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 */
162
+ if(typeof APP.beforeExit !=='function'){
163
+ Object.defineProperties(APP,{
164
+ beforeExit : {
165
+ value : beforeExitApp,
166
+ }
167
+ })
168
+ }
169
+ const backAction = (args) => {
170
+ if(navigationRef && navigationRef.canGoBack()? true : false){
171
+ resetExitCounter();
172
+ navigationRef.goBack(null);
173
+ return false;
174
+ }
175
+ if(isBackConfirmShowing) {
176
+ return;
177
+ }
178
+ if(countBack < MAX_BACK_COUNT){
179
+ countBack++;
180
+ isBackConfirmShowing = false;
181
+ if(countBack === MAX_BACK_COUNT){
182
+ notify.toast({text:'Cliquez à nouveau pour quiiter l\'application'});
84
183
  }
85
- if(typeof onUnmount =='function'){
86
- onUnmount();
184
+ if(countBack === 2 && isPreloaderVisible()) {
185
+ closePreloader();
87
186
  }
187
+ return false;
188
+ }
189
+ isBackConfirmShowing = true;
190
+ return beforeExitApp().finally(x=>{
191
+ isBackConfirmShowing = false;
192
+ }).then(({exit})=>{
193
+ exit();
194
+ })
195
+ };
196
+ const unsubscribeNetInfo = NetInfo.addEventListener(state => {
197
+ APP.setOnlineState(state);
198
+ });
199
+ NetInfo.fetch().catch((e)=>{
200
+ console.log(e," is net info heinn")
201
+ });
202
+ loadResources().finally(()=>{
203
+ (typeof initApp =='function'?initApp : init)({appConfig,contex:{setState}}).then((args)=>{
204
+ if(Auth.isLoggedIn()){
205
+ Auth.loginUser(false);
88
206
  }
89
- },[])
90
-
91
- return (
92
- <SWRConfig
93
- value={{
94
- ...swrConfig,
95
- provider: () => new Map(),
96
- isOnline() {
97
- /* Customize the network state detector */
98
- if(canFetchOffline) return true;
99
- return APP.isOnline();
100
- },
101
- isVisible() {
102
- const screen = activeScreenRef.current;
103
- if(!screen) return false;
104
- if(!screensRef.current[screen]){
105
- screensRef.current[screen] = new Date();
106
- return false;
107
- }
108
- const date = screensRef.current[screen];
109
- const diff = new Date().getTime() - date.getTime();
110
- const timeout = defaultNumber(swrConfig.refreshTimeout,SWR_REFRESH_TIMEOUT)
111
- screensRef.current[screen] = new Date();
112
- return diff >= timeout ? true : false;
113
- },
114
- initFocus(callback) {
115
- let appState = AppState.currentState
116
- const onAppStateChange = (nextAppState) => {
117
- /* If it's resuming from background or inactive mode to active one */
118
- const active = appState.match(/inactive|background/) && nextAppState === 'active';
119
- if (active) {
120
- callback()
121
- }
122
- appState = nextAppState;
123
- appStateRef.current = !!active;
124
- }
125
- // Subscribe to the app state change events
126
- const subscription = AppState.addEventListener('change', onAppStateChange);
127
- return () => {
128
- subscription?.remove()
129
- }
130
- },
131
- initReconnect(cb) {
132
- const callback = ()=>{
133
- cb();
134
- }
135
- /* Register the listener with your state provider */
136
- APP.on(APP.EVENTS.GO_ONLINE,callback);
137
- return ()=>{
138
- APP.off(APP.EVENTS.GO_ONLINE,callback);
139
- }
140
- }
141
- }}
142
- >
143
- <App init={init} render={render}/>
144
- </SWRConfig>
145
- );
146
- };
207
+ setState({
208
+ ...state,hasGetStarted:true,...defaultObj(args && args?.state),hasCallInitApp:true,isInitialized:true,isLoading : false,
209
+ });
210
+ }).catch((e)=>{
211
+ console.error(e," loading resources for app initialization");
212
+ setState({...state,isInitialized:true,hasCallInitApp,isLoading : false,hasGetStarted:false});
213
+ })
214
+ });
147
215
 
148
- setDeviceIdRef.current = ()=>{
149
- return new Promise((resolve,reject)=>{
150
- showPrompt({
151
- title : 'ID unique pour l\'appareil',
152
- maxLength : 30,
153
- defaultValue : appConfig.getDeviceId(),
154
- yes : 'Définir',
155
- placeholder : isMobileNative()? "":'Entrer une valeur unique sans espace SVP',
156
- no : 'Annuler',
157
- onSuccess : ({value})=>{
158
- let message = null;
159
- if(!value || value.contains(" ")){
160
- message = "Merci d'entrer une valeur non nulle ne contenant pas d'espace";
216
+ const Events = {}
217
+ let events = [];
218
+ if(navigationRef && navigationRef.addListener){
219
+ for(let i in Events){
220
+ events.push(navigationRef.addListener(i,Events[i]));
161
221
  }
162
- if(value.length > 30){
163
- message = "la valeur entrée doit avoir au plus 30 caractères";
222
+ }
223
+ APP.onElectron("BEFORE_EXIT",()=>{
224
+ return beforeExitApp().then(({exit})=>{
225
+ exit();
226
+ })
227
+ });
228
+ APP.on(APP.EVENTS.BACK_BUTTON,backAction);
229
+ return () => {
230
+ keyBoardDidShowListener && keyBoardDidShowListener.remove && keyBoardDidShowListener.remove();
231
+ keyBoardDidHideListener && keyBoardDidHideListener.remove && keyBoardDidHideListener.remove();
232
+ if(listener){
233
+ window.visualViewport.removeEventListener('resize', listener);
164
234
  }
165
- if(message){
166
- notify.error(message);
167
- return reject({message})
235
+ APP.off(APP.EVENTS.BACK_BUTTON,backAction);
236
+ if(subscription && subscription.remove){
237
+ subscription.remove();
168
238
  }
169
- resolve(value);
170
- notify.success("la valeur ["+value+"] a été définie comme identifiant unique pour l'application instalée sur cet appareil");
239
+ events.map((ev)=>{
240
+ if(typeof ev =="function") ev();
241
+ })
242
+ unsubscribeNetInfo();
243
+ stopIDLE(false,true);
244
+
245
+ }
246
+ }, []);
247
+ const {isInitialized} = state;
248
+ const isLoading = state.isLoading || !isInitialized || !appReadyRef.current? true : false;
249
+ React.useEffect(()=>{
250
+ if(isInitialized){
251
+ setIsInitialized(true);
252
+ trackIDLE(true);
171
253
  }
172
- })
173
- })
174
- }
254
+ },[isInitialized]);
255
+ const hasGetStarted = state.hasGetStarted !== false? true : false;
256
+ const themeRef = React.useRef(null);
257
+ const [theme,setTheme] = React.useState(themeRef.current || updateTheme(defaultTheme));
258
+ themeRef.current = theme;
259
+ const updatePreferenceTheme = (customTheme,persist)=>{
260
+ setTheme(updateTheme(customTheme));
261
+ };
262
+ const forceRender = React.useForceRender();
263
+ const pref = typeof appPreferences =='function'? appPreferences({setTheme,forceRender,updateTheme:updatePreferenceTheme}) : appPreferences;
264
+ const preferences = React.useMemo(()=>({
265
+ updateTheme:updatePreferenceTheme,
266
+ theme,
267
+ ...defaultObj(pref),
268
+ }),[theme,pref]);
269
+ const isLoaded = !isLoading && state.hasCallInitApp;
270
+ const child = isLoaded ? <NavigationContainer
271
+ ref={navigationRef}
272
+ initialState={initialState}
273
+ {...containerProps}
274
+ onStateChange={(state,...rest) =>{
275
+ setSession(NAVIGATION_PERSISTENCE_KEY,decycle(state),false);
276
+ if(typeof containerProps.onStateChange =='function'){
277
+ containerProps.onStateChange(state,...rest);
278
+ }
279
+ }}
280
+ fallback = {React.isValidElement(containerProps.fallback) ? containerProps.fallback : <Logo.Progress/>}
281
+ >
282
+ <Navigation
283
+ initialRouteName = {defaultStr(hasGetStarted ? appInitialRouteName : getStartedRouteName,"Home")}
284
+ state = {state}
285
+ hasGetStarted = {hasGetStarted}
286
+ isInitialized = {isInitialized}
287
+ onGetStart = {(e)=>{
288
+ setState({...state,hasGetStarted:true})
289
+ }}
290
+ />
291
+ </NavigationContainer> : null;
292
+ const content = null//isLoaded ? typeof render == 'function'? render({children:child,appConfig,config:appConfig}) : child : null;
293
+ return <SafeAreaProvider>
294
+ <GestureHandlerRootView testID={"RN_MainAppGestureHanleRootView"} style={styles.gesture}>
295
+ <AppEntryRootView>
296
+ <PaperProvider
297
+ theme={theme}
298
+ settings={{
299
+ icon: (props) => {
300
+ return <FontIcon {...props}/>
301
+ },
302
+ }}
303
+ >
304
+ <Portal.Host testID="RN_NativePaperPortalHost">
305
+ <ErrorBoundaryProvider/>
306
+ <PreloaderProvider/>
307
+ <DialogProvider responsive testID={"RN_MainAppDialogProvider"}/>
308
+ <AlertProvider SimpleSelect={SimpleSelect}/>
309
+ <FormDataDialogProvider/>
310
+ <BottomSheetProvider/>
311
+ <DropdownAlert ref={notificationRef}/>
312
+ <ErrorBoundary>
313
+ <StatusBar/>
314
+ <SplashScreen isLoaded={isLoaded}>
315
+ <PreferencesContext.Provider value={preferences}>
316
+ {React.isValidElement(content) && content || child}
317
+ </PreferencesContext.Provider>
318
+ </SplashScreen>
319
+ </ErrorBoundary>
320
+ </Portal.Host>
321
+ </PaperProvider>
322
+ </AppEntryRootView>
323
+ </GestureHandlerRootView>
324
+ </SafeAreaProvider>
325
+ }
326
+
327
+ export default App;
328
+
329
+ const styles = StyleSheet.create({
330
+ gesture : {
331
+ flex : 1,
332
+ flexGrow : 1,
333
+ }
334
+ });
@@ -25,6 +25,7 @@ import {getRowsPerPagesLimits} from "./Common/utils";
25
25
  import PropTypes from "prop-types";
26
26
  import {Menu} from "$ecomponents/BottomSheet";
27
27
  import session from "$session";
28
+ import { SWR_REFRESH_TIMEOUT } from "$econtext/utils";
28
29
  import useContext from "$econtext/hooks";
29
30
  import notify from "$cnotify";
30
31
 
@@ -46,15 +47,13 @@ export const setSessionData = (key,value)=>{
46
47
  }
47
48
 
48
49
 
49
-
50
- export const timeout = 5000*60;//5 minutes
51
50
  /***@see : https://swr.vercel.app/docs/api */
52
51
 
53
52
  export const getSWROptions = (defTimeout)=>{
54
- const delay = defaultNumber(defTimeout,timeout);
53
+ const delay = defaultNumber(defTimeout,SWR_REFRESH_TIMEOUT);
55
54
  return {
56
55
  dedupingInterval : delay,
57
- errorRetryInterval : Math.max(delay*2,timeout),
56
+ errorRetryInterval : Math.max(delay*2,SWR_REFRESH_TIMEOUT),
58
57
  errorRetryCount : 5,
59
58
  revalidateOnMount : false,//enable or disable automatic revalidation when component is mounted
60
59
  revalidateOnFocus : true, //automatically revalidate when window gets focused (details)
@@ -27,13 +27,12 @@ const SplashScreenComponent = ({isLoaded,children , duration, delay,logoWidth,lo
27
27
  loadingProgress: new Animated.Value(0),
28
28
  });
29
29
  const { loadingProgress, animationDone} = state;
30
+ const prevIsLoaded = React.usePrevious(isLoaded);
30
31
  React.useEffect(()=>{
31
- if(!isLoaded){
32
- setState({...state,loadingProgress : new Animated.Value(0),animationDone:false});
33
- } else {
32
+ if(isLoaded && !prevIsLoaded){
34
33
  Animated.timing(loadingProgress, {
35
34
  toValue: 100,
36
- duration: duration || 5000,
35
+ duration: duration || 100,
37
36
  delay: delay || 0,
38
37
  useNativeDriver: isNativeMobile(),
39
38
  }).start(() => {
@@ -43,7 +42,7 @@ const SplashScreenComponent = ({isLoaded,children , duration, delay,logoWidth,lo
43
42
  })
44
43
  })
45
44
  }
46
- },[isLoaded]);
45
+ });
47
46
  testID = defaultStr(testID,"RN_SplashscreenComponent")
48
47
  logoWidth = defaultDecimal(logoWidth,150);
49
48
  logoHeight = defaultDecimal(logoHeight,250);
@@ -67,7 +66,7 @@ const SplashScreenComponent = ({isLoaded,children , duration, delay,logoWidth,lo
67
66
  extrapolate: "clamp",
68
67
  }),
69
68
  }
70
- if(animationDone && isLoaded){
69
+ if(isLoaded && animationDone){
71
70
  return React.isValidElement(children)?children:null;
72
71
  }
73
72
  return <>
@@ -4,7 +4,7 @@ import {MD3LightTheme,MD3DarkTheme} from "react-native-paper";
4
4
  import { useMaterial3Theme,isDynamicThemeSupported} from '@pchmn/expo-material3-theme';
5
5
  import { useColorScheme } from 'react-native';
6
6
  import {colorsAlias,Colors} from "$theme";
7
- import {isObj,isNonNullString,defaultStr,extendObj} from "$cutils";
7
+ import {isObj,isNonNullString,defaultStr,extendObj,defaultNumber} from "$cutils";
8
8
  import {getMainScreens} from "$escreens/mainScreens";
9
9
  import {ExpoUIContext} from "./hooks";
10
10
  import {enableAuth,disableAuth} from "$cauth/perms";
@@ -15,6 +15,22 @@ import { prepareScreens } from "./TableData";
15
15
  import {extendFormFields} from "$ecomponents/Form/Fields";
16
16
  import {AuthProvider} from '$cauth';
17
17
  import { signInRef } from "$cauth/authSignIn2SignOut";
18
+ import APP from "$capp/instance";
19
+ import { AppState } from 'react-native'
20
+ import {canFetchOffline} from "$capi/utils";
21
+ import { SWR_REFRESH_TIMEOUT } from "./utils";
22
+ import * as Utils from "$cutils";
23
+ import {setDeviceIdRef} from "$capp";
24
+ import {isMobileNative} from "$cplatform";
25
+ import notify from "$cnotify";
26
+ import {showPrompt} from "$ecomponents/Dialog/confirm";
27
+ import {SWRConfig} from "$swr";
28
+
29
+ Object.map(Utils,(v,i)=>{
30
+ if(typeof v =='function' && typeof window !='undefined' && window && !window[i]){
31
+ window[i] = v;
32
+ }
33
+ });
18
34
 
19
35
  /*****
20
36
  les utilitaires disponibles à passer au provider :
@@ -64,6 +80,7 @@ import { signInRef } from "$cauth/authSignIn2SignOut";
64
80
  realm : {}, //les options de configurations de la base de données realmdb
65
81
  */
66
82
  const Provider = ({children,getTableData,handleHelpScreen,navigation,swrConfig,auth:cAuth,components:cComponents,convertFiltersToSQL,getStructData,tablesData,structsData,...props})=>{
83
+ require('$session');///initializing session
67
84
  const {extendAppTheme} = appConfig;
68
85
  const { theme : pTheme } = useMaterial3Theme();
69
86
  navigation = defaultObj(navigation);
@@ -82,7 +99,62 @@ const Provider = ({children,getTableData,handleHelpScreen,navigation,swrConfig,a
82
99
  appConfig.structsData = appConfig.structsData = isObj(structsData)? structsData : null;
83
100
  getTableData = appConfig.getTable = appConfig.getTableData = getTableOrStructDataCall(tablesData,getTableData);
84
101
  getStructData = appConfig.getStructData = getTableOrStructDataCall(structsData,getStructData);
85
- swrConfig = defaultObj(swrConfig);
102
+
103
+ ///swr config settings
104
+ ///garde pour chaque écran sa date de dernière activité
105
+ const screensRef = React.useRef({});//la liste des écrans actifs
106
+ const isScreenFocusedRef = React.useRef(true);
107
+ const activeScreenRef = React.useRef('');
108
+ const prevActiveScreenRef = React.useRef('');
109
+ const appStateRef = React.useRef({});
110
+ const swrRefreshTimeout = defaultNumber(swrConfig?.refreshTimeout,SWR_REFRESH_TIMEOUT)
111
+ swrConfig = extendObj({
112
+ provider: () => new Map(),
113
+ isOnline() {
114
+ /* Customize the network state detector */
115
+ if(canFetchOffline) return true;
116
+ return APP.isOnline();
117
+ },
118
+ isVisible() {
119
+ const screen = activeScreenRef.current;
120
+ if(!screen) return false;
121
+ if(!screensRef.current[screen]){
122
+ screensRef.current[screen] = new Date();
123
+ return false;
124
+ }
125
+ const date = screensRef.current[screen];
126
+ const diff = new Date().getTime() - date.getTime();
127
+ screensRef.current[screen] = new Date();
128
+ return diff >= swrRefreshTimeout ? true : false;
129
+ },
130
+ initFocus(callback) {
131
+ let appState = AppState.currentState
132
+ const onAppStateChange = (nextAppState) => {
133
+ /* If it's resuming from background or inactive mode to active one */
134
+ const active = appState.match(/inactive|background/) && nextAppState === 'active';
135
+ if (active) {
136
+ callback()
137
+ }
138
+ appState = nextAppState;
139
+ appStateRef.current = !!active;
140
+ }
141
+ // Subscribe to the app state change events
142
+ const subscription = AppState.addEventListener('change', onAppStateChange);
143
+ return () => {
144
+ subscription?.remove()
145
+ }
146
+ },
147
+ initReconnect(cb) {
148
+ const callback = ()=>{
149
+ cb();
150
+ }
151
+ /* Register the listener with your state provider */
152
+ APP.on(APP.EVENTS.GO_ONLINE,callback);
153
+ return ()=>{
154
+ APP.off(APP.EVENTS.GO_ONLINE,callback);
155
+ }
156
+ }
157
+ },swrConfig);
86
158
  if(convertFiltersToSQL !== undefined){
87
159
  appConfig.set("convertFiltersToSQL",convertFiltersToSQL);
88
160
  }
@@ -158,6 +230,36 @@ const Provider = ({children,getTableData,handleHelpScreen,navigation,swrConfig,a
158
230
  }
159
231
  }
160
232
  }
233
+ /**** setDeviceRef */
234
+ setDeviceIdRef.current = ()=>{
235
+ return new Promise((resolve,reject)=>{
236
+ showPrompt({
237
+ title : 'ID unique pour l\'appareil',
238
+ maxLength : 30,
239
+ defaultValue : appConfig.getDeviceId(),
240
+ yes : 'Définir',
241
+ placeholder : isMobileNative()? "":'Entrer une valeur unique sans espace SVP',
242
+ no : 'Annuler',
243
+ onSuccess : ({value})=>{
244
+ let message = null;
245
+ if(!value || value.contains(" ")){
246
+ message = "Merci d'entrer une valeur non nulle ne contenant pas d'espace";
247
+ }
248
+ if(value.length > 30){
249
+ message = "la valeur entrée doit avoir au plus 30 caractères";
250
+ }
251
+ if(message){
252
+ notify.error(message);
253
+ return reject({message})
254
+ }
255
+ resolve(value);
256
+ notify.success("la valeur ["+value+"] a été définie comme identifiant unique pour l'application instalée sur cet appareil");
257
+ }
258
+ })
259
+ })
260
+ }
261
+
262
+
161
263
  const {screens} = navigation;
162
264
  navigation.screens = React.useMemo(()=>{
163
265
  const r = prepareScreens({
@@ -170,6 +272,25 @@ const Provider = ({children,getTableData,handleHelpScreen,navigation,swrConfig,a
170
272
  },[]);
171
273
  navigation.containerProps = defaultObj(navigation.containerProps);
172
274
  const {linking} = navigation;
275
+ React.useEffect(()=>{
276
+ const onScreenFocus = ({sanitizedName})=>{
277
+ prevActiveScreenRef.current = activeScreenRef.current;
278
+ if(activeScreenRef.current){
279
+ screensRef.current[activeScreenRef.current] = null;
280
+ }
281
+ screensRef.current[sanitizedName] = new Date();
282
+ activeScreenRef.current = sanitizedName;
283
+ isScreenFocusedRef.current = true;
284
+ }, onScreenBlur = ()=>{
285
+ isScreenFocusedRef.current = false;
286
+ }
287
+ APP.on(APP.EVENTS.SCREEN_FOCUS,onScreenFocus);
288
+ APP.on(APP.EVENTS.SCREEN_BLUR,onScreenBlur);
289
+ return ()=>{
290
+ APP.off(APP.EVENTS.SCREEN_FOCUS,onScreenFocus);
291
+ APP.off(APP.EVENTS.SCREEN_BLUR,onScreenBlur);
292
+ }
293
+ },[]);
173
294
  return <ExpoUIContext.Provider
174
295
  value={{
175
296
  ...props,
@@ -189,10 +310,11 @@ const Provider = ({children,getTableData,handleHelpScreen,navigation,swrConfig,a
189
310
  getStructData,
190
311
  tablesData,
191
312
  structsData,
192
- appConfig,
193
313
  swrConfig,
194
314
  }}
195
- children={<AuthProvider {...auth} LoginComponent={Login}>{children}</AuthProvider>}
315
+ children={<SWRConfig value={swrConfig}>
316
+ <AuthProvider {...auth} LoginComponent={Login}>{children}</AuthProvider>
317
+ </SWRConfig>}
196
318
  />;
197
319
  }
198
320
  const getTableOrStructDataCall = (tablesOrStructDatas,getTableOrStructDataFunc)=>{
@@ -6,7 +6,6 @@ import { useWindowDimensions } from "$cdimensions";
6
6
  import {isObj,isNonNullString} from "$cutils";
7
7
  import { StyleSheet } from "react-native";
8
8
  import { createContext,useContext as useReactContext } from "react";
9
- import appConfig from "$capp/config";
10
9
 
11
10
  export const ExpoUIContext = createContext(null);
12
11
 
@@ -5,3 +5,4 @@ export {default} from "./hooks";
5
5
  export {default as Provider} from "./Provider";
6
6
 
7
7
  export * from "./TableData";
8
+ export * from "./utils";
@@ -0,0 +1,2 @@
1
+
2
+ export const SWR_REFRESH_TIMEOUT = 5000*60;
@@ -1,315 +0,0 @@
1
- import React from "$react"
2
- import { AppState} from "react-native";
3
- import BackHandler from "$ecomponents/BackHandler";
4
- import * as Linking from 'expo-linking';
5
- import APP from "$capp";
6
- import {AppStateService,trackIDLE,stop as stopIDLE} from "$capp/idle";
7
- import { NavigationContainer} from '@react-navigation/native';
8
- import {navigationRef} from "$cnavigation"
9
- import NetInfo from '$cutils/NetInfo';
10
- import Auth from "$cauth";
11
- import {isNativeMobile,isElectron} from "$cplatform";
12
- import Navigation from "../navigation";
13
- import {set as setSession,get as getSession} from "$session";
14
- import { showConfirm } from "$ecomponents/Dialog";
15
- import {close as closePreloader, isVisible as isPreloaderVisible} from "$epreloader";
16
- import SplashScreen from "$ecomponents/SplashScreen";
17
- import {decycle} from "$cutils/json";
18
- import init from "$capp/init";
19
- import { setIsInitialized} from "$capp/utils";
20
- import {isObj,isNonNullString,isPromise,defaultObj,defaultStr} from "$cutils";
21
- import {loadFonts} from "$ecomponents/Icon/Font";
22
- import appConfig from "$capp/config";
23
- import Preloader from "$preloader";
24
- import {PreloaderProvider} from "$epreloader";
25
- import BottomSheetProvider from "$ecomponents/BottomSheet/Provider";
26
- import DialogProvider from "$ecomponents/Dialog/Provider";
27
- import SimpleSelect from '$ecomponents/SimpleSelect';
28
- import {Provider as AlertProvider} from '$ecomponents/Dialog/confirm/Alert';
29
- import { DialogProvider as FormDataDialogProvider } from '$eform/FormData';
30
- import ErrorBoundaryProvider from "$ecomponents/ErrorBoundary/Provider";
31
- import notify, {notificationRef} from "$notify";
32
- import DropdownAlert from '$ecomponents/Dialog/DropdownAlert';
33
- import { PreferencesContext } from '../Preferences';
34
- import ErrorBoundary from "$ecomponents/ErrorBoundary";
35
- import {updateTheme,defaultTheme} from "$theme";
36
- import StatusBar from "$ecomponents/StatusBar";
37
- import {Provider as PaperProvider,Portal } from 'react-native-paper';
38
- import FontIcon from "$ecomponents/Icon/Font";
39
- import useContext from "$econtext/hooks";
40
- import { GestureHandlerRootView } from 'react-native-gesture-handler';
41
- import { StyleSheet } from "react-native";
42
- import Logo from "$ecomponents/Logo";
43
- import AppEntryRootView from "./RootView";
44
- import { SafeAreaProvider } from 'react-native-safe-area-context';
45
-
46
-
47
- let MAX_BACK_COUNT = 1;
48
- let countBack = 0;
49
- let isBackConfirmShowing = false;
50
-
51
- const resetExitCounter = ()=>{
52
- countBack = 0
53
- isBackConfirmShowing = false;
54
- };
55
-
56
- const NAVIGATION_PERSISTENCE_KEY = 'NAVIGATION_STATE';
57
-
58
- /****
59
- * init {function}: ()=>Promise<{}> est la fonction d'initialisation de l'application
60
- * initialRouteName : la route initiale par défaut
61
- * 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)
62
- */
63
- function App({init:initApp,initialRouteName:appInitialRouteName,render}) {
64
- AppStateService.init();
65
- const {FontsIconsFilter,beforeExit,preferences:appPreferences,navigation,getStartedRouteName} = useContext();
66
- const {containerProps} = navigation;
67
- const [initialState, setInitialState] = React.useState(undefined);
68
- const appReadyRef = React.useRef(true);
69
- const [state,setState] = React.useState({
70
- isLoading : true,
71
- isInitialized:false,
72
- hasCallInitApp : false,
73
- });
74
- React.useEffect(() => {
75
- const loadResources = ()=>{
76
- return new Promise((resolve)=>{
77
- loadFonts(FontsIconsFilter).catch((e)=>{
78
- console.warn(e," ierror loading app resources fonts");
79
- }).finally(()=>{
80
- resolve(true);
81
- });
82
- })
83
- }
84
- const restoreState = () => {
85
- return new Promise((resolve,reject)=>{
86
- (async ()=>{
87
- try {
88
- const initialUrl = await Linking.getInitialURL();
89
- if (isNativeMobile() || initialUrl === null) {
90
- const savedState = getSession(NAVIGATION_PERSISTENCE_KEY);
91
- if (isObj(savedState)) {
92
- setInitialState(savedState);
93
- }
94
- }
95
- } catch(e){ console.log(e," is state error")}
96
- finally {
97
- appReadyRef.current = true;
98
- resolve({});
99
- }
100
- })();
101
- });
102
- };
103
- const subscription = AppState.addEventListener('change', AppStateService.getInstance().handleAppStateChange);
104
- const beforeExitApp = (cb)=>{
105
- return new Promise((resolve,reject)=>{
106
- Preloader.closeAll();
107
- showConfirm({
108
- title : "Quitter l'application",
109
- message : 'Voulez vous vraiment quitter l\'application?',
110
- yes : 'Oui',
111
- no : 'Non',
112
- onSuccess : ()=>{
113
- const foreceExit = ()=>{
114
- BackHandler.exitApp();
115
- if(isElectron() && window.ELECTRON && typeof ELECTRON.exitApp =='function'){
116
- ELECTRON.exitApp({APP});
117
- }
118
- }
119
- const exit = ()=>{
120
- if(typeof beforeExit =='function'){
121
- const r2 = beforeExit()
122
- if(!isPromise(r2)){
123
- throw {message:'La fonction before exit du contexte doit retourner une promesse',returnedResult:r2}
124
- }
125
- return r2.then(foreceExit).catch(reject);
126
- }
127
- foreceExit();
128
- }
129
- const r = {APP,exit};
130
- APP.trigger(APP.EVENTS.BEFORE_EXIT,exit,(result)=>{
131
- if(isObj(result) || Array.isArray(result)){
132
- for(let ik in result){
133
- if(result[ik] === false) return reject({message:'EXIT APP DENIED BY BEFORE EXIT EVENT HANDLER AT POSITON {0}'.sprintf(ik)});
134
- }
135
- }
136
- resolve(r);
137
- if(typeof cb =='function'){
138
- cb(r);
139
- }
140
- });
141
- },
142
- onCancel : reject
143
- })
144
- })
145
- }
146
- /**** 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 */
147
- if(typeof APP.beforeExit !=='function'){
148
- Object.defineProperties(APP,{
149
- beforeExit : {
150
- value : beforeExitApp,
151
- }
152
- })
153
- }
154
- const backAction = (args) => {
155
- if(navigationRef && navigationRef.canGoBack()? true : false){
156
- resetExitCounter();
157
- navigationRef.goBack(null);
158
- return false;
159
- }
160
- if(isBackConfirmShowing) {
161
- return;
162
- }
163
- if(countBack < MAX_BACK_COUNT){
164
- countBack++;
165
- isBackConfirmShowing = false;
166
- if(countBack === MAX_BACK_COUNT){
167
- notify.toast({text:'Cliquez à nouveau pour quiiter l\'application'});
168
- }
169
- if(countBack === 2 && isPreloaderVisible()) {
170
- closePreloader();
171
- }
172
- return false;
173
- }
174
- isBackConfirmShowing = true;
175
- return beforeExitApp().finally(x=>{
176
- isBackConfirmShowing = false;
177
- }).then(({exit})=>{
178
- exit();
179
- })
180
- };
181
- const unsubscribeNetInfo = NetInfo.addEventListener(state => {
182
- APP.setOnlineState(state);
183
- });
184
- NetInfo.fetch().catch((e)=>{
185
- console.log(e," is net info heinn")
186
- });
187
- loadResources().finally(()=>{
188
- (typeof initApp =='function'?initApp : init)({appConfig,contex:{setState}}).then((args)=>{
189
- if(Auth.isLoggedIn()){
190
- Auth.loginUser(false);
191
- }
192
- setState({
193
- ...state,hasGetStarted:true,...defaultObj(args && args?.state),hasCallInitApp:true,isInitialized:true,isLoading : false,
194
- });
195
- }).catch((e)=>{
196
- console.error(e," loading resources for app initialization");
197
- setState({...state,isInitialized:true,hasCallInitApp,isLoading : false,hasGetStarted:false});
198
- })
199
- });
200
-
201
- const Events = {}
202
- let events = [];
203
- if(navigationRef && navigationRef.addListener){
204
- for(let i in Events){
205
- events.push(navigationRef.addListener(i,Events[i]));
206
- }
207
- }
208
- APP.onElectron("BEFORE_EXIT",()=>{
209
- return beforeExitApp().then(({exit})=>{
210
- exit();
211
- })
212
- });
213
- APP.on(APP.EVENTS.BACK_BUTTON,backAction);
214
- return () => {
215
- APP.off(APP.EVENTS.BACK_BUTTON,backAction);
216
-
217
- if(subscription && subscription.remove){
218
- subscription.remove();
219
- }
220
- events.map((ev)=>{
221
- if(typeof ev =="function") ev();
222
- })
223
- unsubscribeNetInfo();
224
- stopIDLE(false,true);
225
-
226
- }
227
- }, []);
228
- const {isInitialized} = state;
229
- const isLoading = state.isLoading || !isInitialized || !appReadyRef.current? true : false;
230
- React.useEffect(()=>{
231
- if(isInitialized){
232
- setIsInitialized(true);
233
- trackIDLE(true);
234
- }
235
- },[isInitialized]);
236
- const hasGetStarted = state.hasGetStarted !== false? true : false;
237
- const themeRef = React.useRef(null);
238
- const [theme,setTheme] = React.useState(themeRef.current || updateTheme(defaultTheme));
239
- themeRef.current = theme;
240
- const updatePreferenceTheme = (customTheme,persist)=>{
241
- setTheme(updateTheme(customTheme));
242
- };
243
- const forceRender = React.useForceRender();
244
- const pref = typeof appPreferences =='function'? appPreferences({setTheme,forceRender,updateTheme:updatePreferenceTheme}) : appPreferences;
245
- const preferences = React.useMemo(()=>({
246
- updateTheme:updatePreferenceTheme,
247
- theme,
248
- ...defaultObj(pref),
249
- }),[theme,pref]);
250
- const isLoaded = !isLoading && state.hasCallInitApp;
251
- const child = isLoaded ? <NavigationContainer
252
- ref={navigationRef}
253
- initialState={initialState}
254
- {...containerProps}
255
- onStateChange={(state,...rest) =>{
256
- setSession(NAVIGATION_PERSISTENCE_KEY,decycle(state),false);
257
- if(typeof containerProps.onStateChange =='function'){
258
- containerProps.onStateChange(state,...rest);
259
- }
260
- }}
261
- fallback = {React.isValidElement(containerProps.fallback) ? containerProps.fallback : <Logo.Progress/>}
262
- >
263
- <Navigation
264
- initialRouteName = {defaultStr(hasGetStarted ? appInitialRouteName : getStartedRouteName,"Home")}
265
- state = {state}
266
- hasGetStarted = {hasGetStarted}
267
- isInitialized = {isInitialized}
268
- onGetStart = {(e)=>{
269
- setState({...state,hasGetStarted:true})
270
- }}
271
- />
272
- </NavigationContainer> : null;
273
- const content = isLoaded ? typeof render == 'function'? render({children:child,appConfig,config:appConfig}) : child : null;
274
- return <SafeAreaProvider>
275
- <GestureHandlerRootView testID={"RN_MainAppGestureHanleRootView"} style={styles.gesture}>
276
- <AppEntryRootView>
277
- <PaperProvider
278
- theme={theme}
279
- settings={{
280
- icon: (props) => {
281
- return <FontIcon {...props}/>
282
- },
283
- }}
284
- >
285
- <Portal.Host testID="RN_NativePaperPortalHost">
286
- <ErrorBoundaryProvider/>
287
- <PreloaderProvider/>
288
- <DialogProvider responsive testID={"RN_MainAppDialogProvider"}/>
289
- <AlertProvider SimpleSelect={SimpleSelect}/>
290
- <FormDataDialogProvider/>
291
- <BottomSheetProvider/>
292
- <DropdownAlert ref={notificationRef}/>
293
- <ErrorBoundary>
294
- <StatusBar/>
295
- <SplashScreen isLoaded={isLoaded}>
296
- <PreferencesContext.Provider value={preferences}>
297
- {React.isValidElement(content) && content || child}
298
- </PreferencesContext.Provider>
299
- </SplashScreen>
300
- </ErrorBoundary>
301
- </Portal.Host>
302
- </PaperProvider>
303
- </AppEntryRootView>
304
- </GestureHandlerRootView>
305
- </SafeAreaProvider>
306
- }
307
-
308
- export default App;
309
-
310
- const styles = StyleSheet.create({
311
- gesture : {
312
- flex : 1,
313
- flexGrow : 1,
314
- }
315
- })