App/src/caj/components/NotLoggedIn (SFConflict jan...

961 lines
27 KiB
TypeScript

import {RegisterProcess, ThemeMode} from '@caj/configs/appVar';
import {appVarActions} from '@caj/configs/appVarReducer';
import {defaultHeaderStyle} from '@caj/configs/colors';
import {SlideFromLeftView} from '@caj/helper/animations';
import {saveVarChanges} from '@caj/helper/appData';
import {apiBackendRequest, makeRequest} from '@caj/helper/request';
import {
accountNameOptions,
EMail,
emailOptions,
passwordOptions,
userNameOptions,
XToken,
} from '@caj/helper/types';
import {RootScreenNavigationProp} from '@caj/Navigation';
import {RootState, store} from '@caj/redux/store';
import {useNavigation} from '@react-navigation/native';
import {
createNativeStackNavigator,
NativeStackNavigationProp,
} from '@react-navigation/native-stack';
import {
Box,
Button,
Center,
Container,
FormControl,
Heading,
HStack,
Icon,
IconButton,
Input,
Pressable,
ScrollView,
Spinner,
Text,
useColorModeValue,
useTheme,
useToast,
VStack,
WarningOutlineIcon,
} from 'native-base';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import {baseFontSize} from 'native-base/lib/typescript/theme/tools';
import {useEffect, useRef, useState} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import reactStringReplace from 'react-string-replace';
import ConfirmationCodeField from './ConfirmationCodeField';
import NameDisplay from './NameDisplay';
import showToast from './Toast';
import {NativeSyntheticEvent, TextInputFocusEventData} from 'react-native';
const validateEmail = (email: EMail) => {
return emailOptions.isAllowed(email);
};
export default function NotLoggedIn() {
const lang = useSelector((state: RootState) => state.appVariables.lang);
const theme = useSelector(
(state: RootState) => state.appVariables.preferences.theme,
);
const toast = useToast();
const navigation = useNavigation<RootScreenNavigationProp>();
return (
<Box alignItems="center" mt={5}>
<Text color="primary.400">{lang.appName}</Text>
<Text color="white.100">{lang.appNameDesc}</Text>
<VStack mt={5} space={4} alignItems="center">
<Button
w="72"
colorScheme="primary"
rounded="xl"
_text={{fontSize: 'xl'}}
onPress={() => {
navigation.navigate('Register', {screen: 'RegStepOne'});
}}>
Sign up
</Button>
<Button
w="72"
colorScheme="black"
variant={theme === ThemeMode.Darkest ? 'outline' : 'subtle'}
rounded="xl"
_text={{fontSize: 'xl', color: 'white.900'}}>
Log in
</Button>
</VStack>
</Box>
);
}
export function RegisterScreenAnim(props: any) {
return (
<SlideFromLeftView>
<RegisterScreen {...props} />
</SlideFromLeftView>
);
}
export type LoginStackNavigatorParamList = {
RegStepOne: undefined;
RegStepTwo: undefined;
RegStepFinal: undefined;
};
const LoginStack = createNativeStackNavigator<LoginStackNavigatorParamList>();
export type LoginScreenNavigationProp =
NativeStackNavigationProp<LoginStackNavigatorParamList>;
function RegisterScreen() {
const lang = useSelector((state: RootState) => state.appVariables.lang);
const theme = useSelector(
(state: RootState) => state.appVariables.preferences.theme,
);
return (
<LoginStack.Navigator>
<LoginStack.Screen
name="RegStepOne"
options={{
animation: 'slide_from_left',
title: lang.account.registration.registration,
headerShown: true,
...defaultHeaderStyle(theme, 'registration'),
}}
component={StepOne}
/>
<LoginStack.Screen
name="RegStepTwo"
options={{
animation: 'slide_from_right',
title: lang.account.registration.registration,
headerShown: true,
...defaultHeaderStyle(theme, 'registration'),
}}
component={StepTwo}
/>
<LoginStack.Screen
name="RegStepFinal"
options={{
animation: 'slide_from_right',
title: lang.account.registration.registration,
headerShown: true,
...defaultHeaderStyle(theme, 'registration'),
}}
component={StepFinal}
/>
</LoginStack.Navigator>
);
}
function Agreement() {
const toast = useToast();
const lang = useSelector((state: RootState) => state.appVariables.lang);
const textColor = useColorModeValue('blue.700', 'cyan.400');
let replacedText = reactStringReplace(
lang.account.registration.info,
'${TermsOfUse}',
(match, i) => (
<Text
key={match + i}
color={textColor}
bold
onPress={() => {
showToast(toast, {
title: lang.account.registration.termsOfUse,
variant: 'solid',
status: 'info',
description: undefined,
isClosable: true,
rest: {colorScheme: 'primary'},
});
}}>
{lang.account.registration.termsOfUse}
</Text>
),
);
replacedText = reactStringReplace(
replacedText,
'${privacyPolicy}',
(match, i) => (
<Text
key={match + i}
color={textColor}
bold
onPress={() => {
showToast(toast, {
title: lang.account.registration.privacyPolicy,
variant: 'solid',
status: 'info',
description: undefined,
isClosable: true,
rest: {colorScheme: 'primary'},
});
}}>
{lang.account.registration.privacyPolicy}
</Text>
),
);
return (
<Text textAlign={'justify'} color={'white.900'}>
{replacedText}
</Text>
);
}
function resendMail(email: EMail, toast: any): Promise<XToken> {
return new Promise<XToken>((resolve, reject) => {
makeRequest({
path: apiBackendRequest.REGISTER_RESEND_MAIL,
requestHeader: {},
request: {
Email: email,
},
//response: {
// XToken: undefined,
//},
})
.then(resp => {
console.log(1);
let token =
store.getState().appVariables.preferences.RegisterProcess.XToken;
if (token !== undefined /*resp.response.XToken !== undefined*/) {
showToast(toast, {
title: store.getState().appVariables.lang.info,
variant: 'solid',
status: 'info',
description:
store.getState().appVariables.lang.account.registration.stepTwo
.resend[2],
isClosable: true,
rest: {},
});
resolve(token);
} else {
reject(500);
showToast(toast, {
title: store.getState().appVariables.lang.error,
variant: 'solid',
status: 'error',
description: 'XToken is undefined',
isClosable: true,
rest: {},
});
}
})
.catch(resp => {
let text = 'unknown error ' + resp.status;
if (resp.status !== undefined) {
const _text =
store.getState().appVariables.lang.account.registration.stepTwo
.resendError[resp.status as number];
if (_text !== undefined) text = _text;
}
showToast(toast, {
title: store.getState().appVariables.lang.error,
variant: 'solid',
status: 'error',
description: text,
isClosable: true,
rest: {},
});
reject(resp.status);
});
});
}
function StepOne() {
const lang = useSelector((state: RootState) => state.appVariables.lang);
const regPro = useSelector(
(state: RootState) => state.appVariables.preferences.RegisterProcess,
);
const dispatch = useDispatch();
const toast = useToast();
const navigation = useNavigation<RootScreenNavigationProp>();
const initNoErrors = {
wrongFormat: false,
alreadyExists: false,
noEntered: false,
unknown: undefined,
};
const [errors, setErrors] = useState(initNoErrors);
const isError =
errors.wrongFormat ||
errors.alreadyExists ||
errors.noEntered ||
errors.unknown !== undefined;
const errorText = () => {
if (errors.wrongFormat) {
return lang.account.registration.stepOne.addressInvalid;
}
if (errors.alreadyExists) {
return lang.account.registration.stepOne.addressExists;
}
if (errors.noEntered) {
return lang.account.registration.stepOne.noMailEntered;
}
if (errors.unknown !== undefined) {
return errors.unknown;
}
};
const [isLoading, setLoading] = useState(false);
const [values, setValues] = useState({email: regPro.EMail});
useEffect(() => {
if (regPro.isRegistering === 'stepTwo') {
setLoading(true);
setErrors(initNoErrors);
setTimeout(nextStep, 500);
} else if (regPro.isRegistering === 'stepFinal') {
setLoading(true);
setErrors(initNoErrors);
setTimeout(nextStep, 500);
}
}, []);
const nextStep = () => {
setLoading(true);
setErrors(initNoErrors);
makeRequest({
path: apiBackendRequest.REGISTER_STEP_1,
requestHeader: {},
request: {
Email: values.email,
},
response: {
XToken: undefined,
},
})
.then(resp => {
let rp = {...regPro};
rp.isRegistering = 'stepTwo';
rp.EMail = values.email;
rp.XToken = resp.response.XToken;
dispatch(appVarActions.setRegisterProcess(rp));
saveVarChanges();
showToast(toast, {
title: lang.account.registration.stepOne.success,
variant: 'solid',
status: 'success',
description: undefined,
isClosable: true,
rest: {colorScheme: 'primary'},
});
navigation.navigate('Register', {screen: 'RegStepTwo'});
setLoading(false);
})
.catch(resp => {
if (resp.status === 401 || resp.status === 204) {
if (regPro.XToken !== undefined) {
let rp = {...regPro};
if (resp.status === 401) {
rp.isRegistering = 'stepTwo';
rp.EMail = values.email;
dispatch(appVarActions.setRegisterProcess(rp));
saveVarChanges();
navigation.navigate('Register', {screen: 'RegStepTwo'});
} else if (resp.status === 204) {
rp.isRegistering = 'stepFinal';
rp.EMail = values.email;
dispatch(appVarActions.setRegisterProcess(rp));
saveVarChanges();
navigation.navigate('Register', {screen: 'RegStepFinal'});
}
setLoading(false);
} else {
resendMail(values.email, toast)
.then(() => {
setLoading(false);
})
.catch(() => {
setLoading(false);
});
}
} else {
showToast(toast, {
title: 'Error',
variant: 'solid',
status: 'error',
description: resp.status,
isClosable: true,
rest: {colorScheme: 'primary'},
});
setLoading(false);
}
});
};
return (
<ScrollView keyboardShouldPersistTaps="handled">
<Box alignItems="center" mt={5}>
<FormControl
w="75%"
maxW="350px"
isRequired
isDisabled={isLoading}
isInvalid={isError}>
<FormControl.Label>
{lang.account.registration.stepOne.title}
</FormControl.Label>
<Input
autoFocus
placeholder={lang.account.registration.stepOne.title}
value={values.email}
autoCapitalize={'none'}
maxLength={emailOptions.maxLength}
keyboardType={'email-address'}
onChangeText={text => {
const mail = text.replaceAll(' ', '');
setValues({email: mail});
if (errors.noEntered && mail !== '') {
let err = errors;
err.noEntered = false;
setErrors({...err});
}
if (errors.wrongFormat && validateEmail(mail)) {
let err = errors;
err.wrongFormat = false;
setErrors({...err});
}
if (errors.alreadyExists) {
let err = errors;
err.alreadyExists = false;
setErrors({...err});
}
}}
/>
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />}>
{errorText()}
</FormControl.ErrorMessage>
</FormControl>
<Container marginY={5}>
<Agreement />
</Container>
<Button
w="75%"
maxW="350px"
colorScheme="primary"
rounded="xl"
_text={{fontSize: 'xl'}}
isLoading={isLoading}
onPress={() => {
if (values.email === '') {
let err = errors;
err.noEntered = true;
setErrors({...err});
} else if (validateEmail(values.email)) {
nextStep();
} else {
let err = errors;
err.wrongFormat = true;
setErrors({...err});
}
}}>
{lang.account.registration.stepOne.button}
</Button>
</Box>
</ScrollView>
);
}
function StepTwo() {
const lang = useSelector((state: RootState) => state.appVariables.lang);
const regPro = useSelector(
(state: RootState) => state.appVariables.preferences.RegisterProcess,
);
const dispatch = useDispatch();
const cellCount = 6;
const {colors} = useTheme();
const toast = useToast();
const navigation = useNavigation<RootScreenNavigationProp>();
const initNoErrors = {
noEntered: false,
};
const [errors, setErrors] = useState(initNoErrors);
const [isLoading, setLoading] = useState(false);
const [values, setValues] = useState({code: ''});
const headerText = () => {
return reactStringReplace(
lang.account.registration.stepTwo.title,
'${EMail}',
(match, i) => (
<Text key={match + i} color={'primary.400'}>
{regPro.EMail}
</Text>
),
);
};
const resendText = () => {
return reactStringReplace(
lang.account.registration.stepTwo.resend[0],
'${resend}',
(match, i) => (
<Text
key={match + i}
color={'primary.400'}
onPress={() => {
setLoading(true);
resendMail(
store.getState().appVariables.preferences.RegisterProcess.EMail,
toast,
)
.then(() => {
setLoading(false);
})
.catch(() => {
setLoading(false);
});
}}>
{lang.account.registration.stepTwo.resend[1]}
</Text>
),
);
};
const validate = (text: string) => {
setLoading(true);
setTimeout(() => {
makeRequest({
path: apiBackendRequest.REGISTER_STEP_2,
requestHeader: {},
requestGET: {':verifyId': text, ':xToken': regPro.XToken || ''},
})
.then(resp => {
let rp = {...regPro};
rp.isRegistering = 'stepFinal';
dispatch(appVarActions.setRegisterProcess(rp));
saveVarChanges();
showToast(toast, {
title: lang.account.registration.stepTwo.success,
variant: 'solid',
status: 'success',
description: undefined,
isClosable: true,
rest: {colorScheme: 'primary'},
});
navigation.navigate('Register', {screen: 'RegStepFinal'});
setLoading(false);
})
.catch(resp => {
let text = 'unknown error ' + resp.status;
if (resp.status !== undefined) {
const _text =
lang.account.registration.stepTwo.verificationError[
resp.status as number
];
if (_text !== undefined) text = _text;
}
showToast(toast, {
title: lang.error,
variant: 'solid',
status: 'error',
description: text,
isClosable: true,
rest: {},
});
setLoading(false);
if (resp.status === 422) {
let rp = {...regPro};
rp.isRegistering = false;
dispatch(appVarActions.setRegisterProcess(rp));
saveVarChanges();
navigation.navigate('Register', {screen: 'RegStepOne'});
}
});
}, 500);
};
return (
<ScrollView keyboardShouldPersistTaps="handled">
<Box alignItems="center" mt={5}>
<Center w="75%" maxW="1000px">
<Text>{headerText()}</Text>
<Box>
<FormControl isDisabled={isLoading} isInvalid={errors.noEntered}>
<ConfirmationCodeField
cellCount={cellCount}
charType="number"
disabled={isLoading}
rest={{mt: 5}}
onChange={(text: string) => {
setValues({code: text});
setErrors(initNoErrors);
}}
onFinish={(text: string) => validate(text)}
/>
<FormControl.ErrorMessage
leftIcon={<WarningOutlineIcon size="xs" />}>
{lang.account.registration.stepTwo.noCodeEntered}
</FormControl.ErrorMessage>
</FormControl>
</Box>
</Center>
<Container marginY={5}>
<Agreement />
</Container>
<Button
w="75%"
maxW="350px"
colorScheme="primary"
rounded="xl"
_text={{fontSize: 'xl'}}
isLoading={isLoading}
onPress={() => {
if (values.code.length === cellCount) {
validate(values.code);
} else {
let err = {...errors};
err.noEntered = true;
setErrors(err);
}
}}>
{lang.account.registration.stepTwo.button}
</Button>
<Container marginY={5}>
<Text textAlign={'justify'} color={'white.900'}>
{resendText()}
</Text>
</Container>
</Box>
</ScrollView>
);
}
function StepFinal() {
const lang = useSelector((state: RootState) => state.appVariables.lang);
const regPro = useSelector(
(state: RootState) => state.appVariables.preferences.RegisterProcess,
);
const dispatch = useDispatch();
const {colors} = useTheme();
const toast = useToast();
const navigation = useNavigation<RootScreenNavigationProp>();
const [isLoading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
interface inputElementType {
label: string;
input: string;
autoCapitalize: 'none' | 'words';
errorIndex: 'none' | string;
errorTextObject: any;
isPassword: boolean | 'passwordRepeat';
minLength: number;
maxLength: number;
onTextChange: any;
textChangeTimeout: number;
isAllowed: any;
}
const accountNameRef = useRef(setTimeout(() => {}));
const accountNameFetchRef = useRef(setTimeout(() => {}));
const accountName = {
label: lang.account.registration.stepFinal.accountName,
input: '',
errorIndex: 'none',
errorTextObject: lang.account.registration.stepFinal.accountNameError,
minLength: accountNameOptions.minLength,
maxLength: accountNameOptions.maxLength,
isAllowed: accountNameOptions.isAllowed,
isPassword: false,
onTextChange: (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
let text = e.nativeEvent.text;
let self = accountName;
clearTimeout(accountNameRef.current);
let obj = {...valuesAccountName};
accountNameRef.current = setTimeout(() => {
obj.input = text;
if (text.length < self.minLength) obj.errorIndex = 'tooShort';
else if (text.length > self.maxLength) obj.errorIndex = 'tooLong';
else if (self.isAllowed(text) === false) obj.errorIndex = 'invalid';
else obj.errorIndex = 'none';
setValuesAccountName(obj);
}, 50);
clearTimeout(accountNameFetchRef.current);
accountNameFetchRef.current = setTimeout(() => {
console.log(obj);
if (obj.errorIndex === 'none') {
makeRequest({
path: apiBackendRequest.REGISTER_STEP_FINAL_ACCOUNT_NAME_CHECK,
request: {AccountName: obj.input},
})
.then(resp => {
console.log('OK');
})
.catch(resp => {
if (resp.status !== undefined) {
obj.errorIndex = resp.status;
setValuesAccountName(obj);
}
});
}
}, 750);
},
} as inputElementType;
const [valuesAccountName, setValuesAccountName] = useState(accountName);
const userNameRef = useRef(setTimeout(() => {}));
const userName = {
label: lang.account.registration.stepFinal.userName,
input: '',
errorIndex: 'none',
errorTextObject: lang.account.registration.stepFinal.userNameError,
minLength: userNameOptions.minLength,
maxLength: userNameOptions.maxLength,
isAllowed: userNameOptions.isAllowed,
isPassword: false,
onTextChange: (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
let text = e.nativeEvent.text;
let self = userName;
clearTimeout(userNameRef.current);
userNameRef.current = setTimeout(() => {
let obj = {...valuesUserName};
obj.input = text;
if (text.length < self.minLength) obj.errorIndex = 'tooShort';
else if (text.length > self.maxLength) obj.errorIndex = 'tooLong';
else if (self.isAllowed(text) === false) obj.errorIndex = 'invalid';
else obj.errorIndex = 'none';
setValuesUserName(obj);
}, 50);
},
} as inputElementType;
const [valuesUserName, setValuesUserName] = useState(userName);
const passwordRef = useRef(setTimeout(() => {}));
const password = {
label: lang.account.registration.stepFinal.password,
input: '',
errorIndex: 'none',
errorTextObject: lang.account.registration.stepFinal.passwordError,
minLength: passwordOptions.minLength,
maxLength: passwordOptions.maxLength,
isAllowed: passwordOptions.isAllowed,
isPassword: true,
onTextChange: (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
let text = e.nativeEvent.text;
let self = password;
clearTimeout(passwordRef.current);
passwordRef.current = setTimeout(() => {
let obj = {...valuesPassword};
obj.input = text;
if (text.length < self.minLength) obj.errorIndex = 'tooShort';
else if (text.length > self.maxLength) obj.errorIndex = 'tooLong';
else if (self.isAllowed(text) === false) obj.errorIndex = 'invalid';
else obj.errorIndex = 'none';
setValuesPassword(obj);
let objRe = {...valuesPasswordRe};
if (text !== valuesPasswordRe.input) objRe.errorIndex = 'noMatch';
else objRe.errorIndex = 'none';
setValuesPasswordRe(objRe);
}, 50);
},
} as inputElementType;
const [valuesPassword, setValuesPassword] = useState(password);
const passwordReRef = useRef(setTimeout(() => {}));
const passwordRe = {
label: lang.account.registration.stepFinal.passwordRepeat,
input: '',
errorIndex: 'none',
errorTextObject: lang.account.registration.stepFinal.passwordError,
minLength: passwordOptions.minLength,
maxLength: passwordOptions.maxLength,
isAllowed: passwordOptions.isAllowed,
isPassword: 'passwordRepeat',
onTextChange: (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
let text = e.nativeEvent.text;
let self = passwordRe;
clearTimeout(passwordReRef.current);
passwordReRef.current = setTimeout(() => {
let obj = {...valuesPasswordRe};
obj.input = text;
console.log(text, valuesPassword.input);
if (text !== valuesPassword.input) obj.errorIndex = 'noMatch';
else obj.errorIndex = 'none';
setValuesPasswordRe(obj);
}, 50);
},
} as inputElementType;
const [valuesPasswordRe, setValuesPasswordRe] = useState(passwordRe);
const inputElement = (
val: inputElementType,
set: React.Dispatch<React.SetStateAction<inputElementType>>,
valConst: inputElementType,
autofocus?: boolean,
) => {
const isPassword =
val.isPassword === true || val.isPassword === 'passwordRepeat';
return (
<FormControl
w="75%"
maxW="350px"
isRequired
isDisabled={isLoading}
isInvalid={val.errorIndex !== 'none'}>
<FormControl.Label>{val.label}</FormControl.Label>
<Input
autoFocus={autofocus}
type={isPassword && showPassword === false ? 'password' : 'text'}
placeholder={''}
keyboardType={'default'}
onChange={valConst.onTextChange}
InputRightElement={
isPassword ? (
<IconButton
mr="2"
onPress={() => setShowPassword(!showPassword)}
icon={
<MaterialIcons
size={20}
name={showPassword ? 'visibility' : 'visibility-off'}
/>
}
borderRadius="full"
/>
) : undefined
}
/>
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />}>
{val.errorTextObject[val.errorIndex] !== undefined
? val.errorTextObject[val.errorIndex]
.replaceAll('$(minLength)', val.minLength)
.replaceAll('$(maxLength)', val.maxLength)
: val.errorTextObject[val.errorIndex] !== 'none'
? lang.error
: null}
</FormControl.ErrorMessage>
</FormControl>
);
};
return (
<ScrollView keyboardShouldPersistTaps="handled">
<VStack space={3} alignItems="center" mt={5}>
<Container maxWidth={'100%'} alignItems={'center'}>
<Center>
<Text>{lang.account.registration.stepFinal.displayName}</Text>
<NameDisplay
UserName={
valuesUserName.input !== ''
? valuesUserName.input
: lang.account.registration.stepFinal.userName
}
AccountName={
valuesAccountName.input !== ''
? valuesAccountName.input
: lang.account.registration.stepFinal.accountName
}
/>
</Center>
</Container>
{inputElement(valuesUserName, setValuesUserName, userName)}
{inputElement(valuesAccountName, setValuesAccountName, accountName)}
{inputElement(valuesPassword, setValuesPassword, password)}
{inputElement(valuesPasswordRe, setValuesPasswordRe, passwordRe)}
</VStack>
<Center>
<Container mt={5}>
<Agreement />
</Container>
<Button
marginY={5}
w="75%"
maxW="350px"
colorScheme="primary"
rounded="xl"
_text={{fontSize: 'xl'}}
isLoading={isLoading}
onPress={() => {}}>
{lang.account.registration.stepFinal.button}
</Button>
</Center>
</ScrollView>
);
}