@fto-consult/expo-ui
Version:
Bibliothèque de composants UI Expo,react-native
473 lines (459 loc) • 19.9 kB
JavaScript
import React from "$react";
import {isNonNullString,isObj,defaultNumber,defaultStr,uniqid,extendObj,isFunction} from "$cutils";
import {navigate} from "$cnavigation";
import FormData from "$ecomponents/Form/FormData/FormData";
import { Action } from "$ecomponents/Form";
import {getForm} from "$ecomponents/Form/utils";
import Button from "$ecomponents/Button";
import notify from "$notify";
import {StyleSheet} from "react-native";
import { useAuth } from "$cauth/AuthProvider";
import Preloader from "$epreloader";
import Dimensions from "$cplatform/dimensions";
import theme from "$theme";
import Label from "$ecomponents/Label";
import View from "$ecomponents/View";
import Avatar from "$ecomponents/Avatar";
import Surface from "$ecomponents/Surface";
import ScreenWithoutAuthContainer from "$escreen/ScreenWithoutAuthContainer";
import {getTitle} from "$escreens/Auth/utils";
import {isWeb} from "$cplatform";
import ProviderSelector from "./ProviderSelector";
import { ScrollView } from "react-native";
import PropTypes from "prop-types";
import useContext from "$econtext/hooks";
const WIDTH = 400;
export default function LoginComponent(props){
let {formName,step,appBarProps,onSuccess,withPortal,testID} = props;
const {auth:{loginPropsMutator,Login}} = useContext();
let loginTitle = getTitle();
testID = defaultStr(testID,"RN_Auth.LoginComponent");
formName = React.useRef(uniqid(defaultStr(formName,"login-formname"))).current;
const nextButtonRef = React.useRef(null);
const previousButtonRef = React.useRef(null);
const backgroundColor = theme.colors.surface;
const _getForm = x=> getForm(formName);
const isMounted = React.useIsMounted();
const initializeRef = React.useRef(props.initialize);
const onStepChangeRef = React.useRef(props.onStepChange);
const auth = useAuth();
const notifyUser = (message,title)=> {
if(isObj(message)){
return notify.error({...message,position:'top'})
} else if(typeof message =='object') return false;
return notify.error({message,title,position:'top'});
}
const [state,_setState] = React.useState({
step : defaultNumber(step,1),
data : defaultObj(props.data),
});
const setState = (state2)=>{
if(!isMounted()) return;
state2 = isObj(state2)? state2 : {};
return _setState({...state,...state2});
}
const getData = ()=>{
const form = _getForm();
if(form && form.getData){
return extendObj({},state.data,form.getData());
}
return defaultObj(props.data);
}
const goToFirstStep = ()=>{
setState({step:1,data:getData()});
}
const focusField = (fieldName)=>{
const form = _getForm();
if(form){
const field = form.getField(fieldName);
if(field){
field.focus();
}
}
}
if(withPortal){
appBarProps = defaultObj(appBarProps);
appBarProps.backAction = false;
}
React.useEffect(()=>{
if(withPortal && isWeb() && typeof document !== 'undefined'){
setTimeout(()=>{
document.title = loginTitle
},1000)
}
},[withPortal]);
React.useEffect(()=>{
Preloader.closeAll();
/*** pour initializer les cordonnées du composant login */
if(typeof initializeRef.current =='function'){
initializeRef.current();
}
},[]);
const prevStep = React.usePrevious(state.step);
React.useEffect(()=>{
/*** lorsque le state du composant change */
if(typeof onStepChangeRef.current =='function'){
return onStepChangeRef.current({...state,previousStep:prevStep,focusField,nextButtonRef})
}
},[state.step]);
const getButtonAction = React.useMemo(()=>{
return (buttonRef)=>{
buttonRef = buttonRef || React.createRef();
return {
ref : buttonRef,
isDisabled : x=> typeof buttonRef?.current?.isDisabled ==="function" && buttonRef.current?.isDisabled(),
enable : x=>{
return typeof buttonRef?.current?.enable =="function" && buttonRef.current.enable();
},
disable : x=> {
return typeof buttonRef?.current?.disable =="function" && buttonRef?.current.disable()
},
}
}
},[]);
const nextButton = getButtonAction(nextButtonRef),
prevButton = getButtonAction(previousButtonRef);
const setIsLoading = (buttonRef,bool)=>{
if(typeof buttonRef?.current?.setIsLoading == 'function'){
return buttonRef.current?.setIsLoading(bool)
}
}
const beforeSubmitRef = React.useRef(null);
const canSubmitRef = React.useRef(null);
const onSuccesRef = React.useRef(null);
const signIn = ()=>{
const form = _getForm();
if(!form){
notifyUser("Impossible de valider le formulaire car celui-ci semble invalide")
return;
}
if(!form.isValid()){
notifyUser(form.getErrorText());
return;
}
const data = getData();
const canSubmit = typeof canSubmitRef.current == 'function'? canSubmitRef.current : w=>true;
const beforeSubmit = typeof beforeSubmitRef.current === 'function' ? beforeSubmitRef.current : ()=> true;
const args = {...state,data,form,state,setState,nextButtonRef,previousButtonRef};
const cS = canSubmit(args);
if(typeof cS === 'string' && cS){
return notifyUser(cS);
}
if(cS !== false && beforeSubmit(args) !== false){
Preloader.open("vérification ...");
setIsLoading(nextButtonRef,true);
return auth.signIn(data).then((result)=>{
if(typeof onSuccesRef.current =='function' && onSuccesRef.current({data,result})=== false) return;
if(isFunction(onSuccess)){
onSuccess({data,result});
} else {
navigate("Home");
}
}).finally(()=>{
Preloader.close();
setIsLoading(nextButtonRef,false);
})
}
}
const mediaQueryUpdateStyle = ()=>{
return StyleSheet.flatten([updateMediaQueryStyle()]);
};
let withScrollView = typeof props.withScrollView =='boolean'? props.withScrollView : true;
const Wrapper = withPortal ? ScreenWithoutAuthContainer : withScrollView ? ScrollView: View;
if(React.isComponent(Login)) return <Login
{...props}
withScreen = {withPortal}
withScrollView = {withScrollView}
Wrapper = {Wrapper}
wrapperProps = {withPortal ? {appBarProps,authRequired:false,title:loginTitle,withScrollView} : {style:[styles.wrapper]}}
appBarProps = {appBarProps}
onSuccess = {onSuccess}
auth = {auth}
formName = {formName}
/***
* permet de connecter un utilisatgeur au backend
* @param {object} data, la données liée à l'utilisateur à connecter
* @param {object} options, les options de connexion
* @return {object}, la données résultat à la fonction de connexion de l'utilisateur
*/
signIn = {(data,options,...rest)=>{
options = defaultObj(options);
if(!isObj(data) || !Object.size(data,true)){
data = getData();
}
Preloader.open("Connexion ...");
return auth.signIn(data,...rest).then((result)=>{
if(typeof options.onSuccess === "function"){
if(options.onSuccess({data,result}) === false) return;
} else if(typeof options.callback === "function" && options.callback({data,result}) === false){
return;
}
if(isFunction(onSuccess) && onSuccess({data,result}) === false){
} else {
navigate("Home");
}
return result;
}).finally(()=>{
Preloader.close();
});
}}
mediaQueryUpdateStyle={mediaQueryUpdateStyle}
/>
const callArgs = {
...state,
getButtonAction,
data : getData(),
mediaQueryUpdateStyle,
signIn,
setState,
setIsLoading,
state,
nextButton,
prevButton,
previousButton:prevButton,
showError : notifyUser,
notifyUser,
notify,
getForm,
getData,
focusField,
formName,
nextButtonRef,
ProviderSelector,
previousButtonRef,
};
const {header : Header,
headerTopContent:HeaderTopContent,
containerProps : customContainerProps,
contentProps : customContentProps,
formProps,
wrapperProps : cWrapperProps,
title : customTitle,
withScrollView:customWithScrollView,children,initialize,renderNextButton,renderPreviousButton,data:loginData,canGoToNext,keyboardEvents,onSuccess:onLoginSuccess,beforeSubmit:beforeSubmitForm,canSubmit:canSubmitForm,onStepChange,...loginProps} = loginPropsMutator(callArgs);
if(typeof initialize =="function"){
initializeRef.current = initialize;
}
if(typeof onStepChange =="function"){
onStepChangeRef.current = onStepChange;
}
withScrollView = typeof customWithScrollView =='boolean'? customWithScrollView : withScrollView;
if(isNonNullString(customTitle)){
loginTitle = customTitle;
}
const containerProps = defaultObj(customContainerProps);
const contentProps = defaultObj(customContentProps);
/****la fonction à utiliser pour vérifier si l'on peut envoyer les données pour connextion
* par défaut, on envoie les données lorssqu'on est à l'étappe 2
* **/
canSubmitRef.current = typeof canSubmitForm =='function'? canSubmitForm : ({step})=>step >= 2;
beforeSubmitRef.current = typeof beforeSubmitForm =='function'? beforeSubmitForm : x=> true;
onSuccesRef.current = onLoginSuccess;
const goToNext = ()=>{
let step = state.step;
const data = getData();
const form = _getForm();
if(!form){
notifyUser("Impossible de valider le formulaire car celui-ci semble invalide")
return;
}
if(!form.isValid()){
notifyUser(form.getErrorText());
return;
}
const args = {...state,data,form,state,step,setState,nextButtonRef,previousButtonRef};
if(nextButtonRef.current && nextButtonRef.current.isDisabled()){
return;
}
if(typeof canGoToNext =='function'){
const s = canGoToNext(args);
if(s === false) {
nextButtonRef.current?.disable();
return;
}
if(isNonNullString(s)){
notifyUser(s);
nextButtonRef.current?.disable();
return
}
nextButtonRef.current?.enable();
}
if(step > 1){
signIn();
} else {
setState({...state,step:step+1,data})
}
}
const loginFields = {};
let hasLoginFields = false;
Object.map(loginProps.fields,(field,i)=>{
if(isObj(field)){
if(typeof field.step ==='number' && field.step !== state.step){}
else {
loginFields[i] = Object.clone(field);
hasLoginFields = true;
if("autoFocusOnStep" in loginFields[i] && typeof loginFields[i].autoFocus !=='boolean'){
loginFields[i].autoFocus = !!loginFields[i].autoFocusOnStep;
}
}
}
});
const wProps = defaultObj(typeof cWrapperProps =="function"? cWrapperProps({...callArgs,withPortal,withScreen:withPortal,withScrollView,state,formName}) : cWrapperProps);
const wrapperProps = withPortal ? {appBarProps,authRequired:false,title:loginTitle,withScrollView,...wProps} : { ...wProps,style:[styles.wrapper,wProps.style]};
const sH = React.isComponent(HeaderTopContent)? <HeaderTopContent mediaQueryUpdateStyle = {mediaQueryUpdateStyle} /> : React.isValidElement(HeaderTopContent)? HeaderTopContent : null;
const header = React.isComponent(Header) ? <Header mediaQueryUpdateStyle = {mediaQueryUpdateStyle}/> : React.isValidElement(Header)? Header : null;
return <Wrapper testID = {testID+"_Wrapper" }{...wrapperProps}>
{sH}
<Surface {...containerProps} {...defaultObj(loginProps?.containerProps)} style={[styles.container,{backgroundColor},containerProps.style,loginProps?.containerProps?.style]} testID={testID+"_LoginContainer"}>
<Surface elevation = {0} {...contentProps} mediaQueryUpdateStyle = {mediaQueryUpdateStyle} {...contentProps} testID={testID+"_LoginContent"} style={[styles.content,{backgroundColor},contentProps.style]}>
<FormData
formName = {formName}
testID = {testID+"_FormData"}
style = {[styles.formData,{backgroundColor}]}
header = {React.isValidElement(header)? header : <View testID={`${testID}_HeaderContainer`} style = {[styles.header]}>
<Avatar testID={testID+"_Avatar"} size={50} secondary icon = 'lock'/>
<Label testID={testID+"_HeaderText"} bool style={{color:theme.colors.primaryOnSurface,fontSize:18,paddingTop:10}}>Connectez vous SVP</Label>
</View>}
responsive = {false}
{...loginProps}
fields = {loginFields}
formProps = {extendObj(true,{},{
keyboardEvents : {
enter : ({formInstance})=>{
goToNext();
},
}
},formProps)}
data = {extendObj(state.data,loginData)}
>
<>
{renderNextButton !== false || renderPreviousButton !== false ? <>
{hasLoginFields?<View testID={testID+"_ButtonsContainer"} style={[styles.buttonWrapper]}>
{renderNextButton !== false ? <Action
ref = {nextButtonRef}
primary
formName={formName}
mode = "contained"
rounded
style = {styles.button}
onPress = {goToNext}
icon = {state.step == 1? 'arrow-right':'login'}
surface
testID = {testID+"_NextButton"}
>
{state.step == 1? 'Suivant' : 'Connexion' }
</Action> : null}
{renderPreviousButton !== false && state.step>=2 ? <Button
onPress = {goToFirstStep}
ref = {previousButtonRef}
mode = "contained"
rounded
raised
style = {styles.button}
secondary
surface
icon = {'arrow-left'}
testID = {testID+"_PrevButton"}
>
Précédent
</Button> : null}
</View> : null}
</> : null}
{React.isValidElement(children) ? children : null}
</>
</FormData>
{React.isValidElement(contentProps.children) ? contentProps.children : null}
</Surface>
{React.isValidElement(containerProps.children) ? containerProps.children : null}
</Surface>
{React.isValidElement(wrapperProps.children) ? wrapperProps.children : null}
</Wrapper>;
}
const updateMediaQueryStyle = ()=>{
const isSmallPhone = Dimensions.isSmallPhoneMedia(),isTablet = Dimensions.isTabletMedia(),
isMobile = Dimensions.isMobileMedia(),isDesktop = Dimensions.isDesktopMedia();
const {width} = Dimensions.get("window");
return {
width : isSmallPhone ? "95%" : isMobile?"90%" : isTablet ? "50%" : Math.min(WIDTH,(35*width)/100),
minWidth : isTablet || isDesktop ? WIDTH : undefined,
};
}
const styles = StyleSheet.create({
wrapper : {
flex:1,
width : '100%',
height : '100%',
},
portalContainer : {
...StyleSheet.absoluteFillObject,
flex:1,
left:0,
zIndex : 1000,
top : 0,
},
container : {
justifyContent : 'center',
alignItems : 'center',
width : '100%',
height : '100%',
paddingVertical : 15,
flex : 1,
},
content : {
width : 300,
paddingVertical : 40,
paddingHorizontal : 20,
justifyContent : 'center',
//alignItems : 'center',
flex : 1,
},
button : {
//maxWidth : 130,
margin : 10,
},
header : {
flexDirection:'column',
width : '100%',
alignItems : 'center',
},
formData : {
justifyContent : 'flex-start',
width : '100%',
},
buttonWrapper : {
justifyContent : 'center',
width : '100%'
}
});
LoginComponent.propTypes = {
/****
les props du composant Wrapper, peut être une fonction où un objet
- s'il s'agit d'une fonction : elle est définie comme suit :
({withScreen<boolean>,withPortal<boolean>,withScrolView<boolean>})=> <object>,
*/
wrapperProps : PropTypes.oneOfType([
PropTypes.func,
PropTypes.object,
]),
/***
les props du composant Container, enfant hiérachique au composant Wrapper
idem aux props du composant Surface
*/
containerProps : PropTypes.shape(defaultObj(Surface.propTypes)),
//les props du composant Surface, parent direct du composant FormData utile pour le rendu du form
contentProps : PropTypes.shape(defaultObj(Surface.propTypes)),
//les props du form data, idem à ceux du composant FormData
formProps : PropTypes.object,
headerTopContent : PropTypes.oneOfType([
PropTypes.func,
PropTypes.node,
PropTypes.element,
]),
header : PropTypes.oneOfType([
PropTypes.node,
PropTypes.element,
PropTypes.func,
]),
onSuccess : PropTypes.func, //la fonctino appelée lorsque l'utilisateur a été connecté, lorsque l'action liée à la fonction signIn de auth s'est terminée correctement
renderNextButton : PropTypes.bool,//si le bouton next sera rendu
renderPreviousButton : PropTypes.bool,//si le bouton previous sera rendu
}