Loading packages/shop-mobile-expo/src/screens/authentication/Home.screen.tsx 0 → 100644 +149 −0 Original line number Diff line number Diff line import React from 'react'; import { Text, View, Image, StyleSheet } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { Button } from 'react-native-paper'; // tslint:disable-next-line: no-implicit-dependencies no-submodule-imports import AntDesignIcon from '@expo/vector-icons/AntDesign'; // ACTIONS & SELECTORS import { useAppSelector } from '../../store/hooks'; import { getLanguage } from '../../store/features/translation'; // COMPONENTS import { FocusAwareStatusBar, PaperText } from '../../components/Common'; // STYLES import { GLOBAL_STYLE as GS, CONSTANT_SIZE as CS, CONSTANT_COLOR as CC, } from '../../assets/ts/styles'; const STYLES = StyleSheet.create({ container: { ...GS.screen, ...GS.bgTransparent, ...GS.px5, ...GS.pb5, ...GS.mx5, zIndex: 1, }, titleLogoContainer: { ...GS.centered, flex: 1, }, logoImg: { ...GS.w100, height: 100, marginBottom: -20 }, logoTitle: { ...GS.txtCapitalize, fontSize: CS.FONT_SIZE + 1, opacity: 0.7, }, networkBtnFacebook: { flex: 1, backgroundColor: CC.facebook }, networkBtnGoogle: { flex: 1, backgroundColor: CC.google }, }); const HomeAuthScreen = () => { // SELECTORS const currentLanguage = useAppSelector(getLanguage); // NAVIGATION const navigation = useNavigation(); // FUNCTIONS const onPressSignUpByAddress = () => { navigation.navigate('STACK/SIGN_UP_BY_ADDRESS' as never); }; return ( <View style={{ ...GS.screen }}> <FocusAwareStatusBar translucent={false} backgroundColor={CC.primary} barStyle='light-content' /> <View style={STYLES.container}> {/* title logo */} <View style={STYLES.titleLogoContainer}> <Image source={require('../../assets/img/ever/logo.png')} resizeMode='contain' style={STYLES.logoImg} /> <Text style={STYLES.logoTitle}> {currentLanguage.INVITE_VIEW.BY_CODE.LOGO.DETAILS} </Text> </View> {/* Social Networks buttons */} <View style={{ ...GS.py4 }}> <View> <Button mode='contained' style={{ ...GS.bgSecondary, ...GS.mb2, }} labelStyle={{ ...GS.txtCapitalize, ...GS.py1 }} onPress={() => onPressSignUpByAddress()}> {currentLanguage.INVITE_VIEW.GET_IN_BY_ADDRESS} </Button> <View style={{ ...GS.inlineItems, ...GS.centered }}> <Button mode='contained' style={STYLES.networkBtnFacebook}> <AntDesignIcon name='facebook-square' color={CC.light} size={CS.FONT_SIZE * 2} /> </Button> <View style={{ ...GS.mr2 }} /> <Button mode='contained' style={STYLES.networkBtnGoogle}> <AntDesignIcon name='google' color={CC.light} size={CS.FONT_SIZE * 2} /> </Button> </View> </View> <View style={{ ...GS.py4, ...GS.my3, ...GS.centered }}> <PaperText style={{ ...GS.txtLower, color: CC.gray, fontSize: CS.FONT_SIZE + 3, }}> {currentLanguage.OR}{' '} <Text style={{ ...GS.fontBold, color: CC.light }}> {currentLanguage.INVITE_VIEW.BY_CODE.OR_WHAT} </Text> </PaperText> </View> <View> <Button mode='outlined' style={{ ...GS.py1, borderColor: CC.gray }} labelStyle={{ ...GS.txtCapitalize, color: CC.gray, fontSize: CS.FONT_SIZE + 3, }}> {currentLanguage.INVITE_VIEW.BY_CODE.INVITE_CODE} </Button> </View> </View> </View> </View> ); }; export default HomeAuthScreen; packages/shop-mobile-expo/src/screens/authentication/SignIn.screen.tsx 0 → 100644 +12 −0 Original line number Diff line number Diff line import React from 'react'; import { Text, View } from 'react-native'; export interface Props {} const SignInScreen: React.FC<Props> = ({}) => ( <View> <Text>SignInScreen</Text> </View> ); export default SignInScreen; packages/shop-mobile-expo/src/screens/authentication/SignUp.screen.tsx 0 → 100644 +12 −0 Original line number Diff line number Diff line import React from 'react'; import { Text, View } from 'react-native'; export interface Props {} const SignUpScreen: React.FC<Props> = () => ( <View> <Text>SignUpScreen</Text> </View> ); export default SignUpScreen; packages/shop-mobile-expo/src/screens/authentication/SignUpByAddress.screen.tsx 0 → 100644 +650 −0 Original line number Diff line number Diff line import React from 'react'; import { View, StyleSheet, ScrollView, Alert, TouchableOpacity, // Button as NativeBtn, TextInput as NativeTextInput, } from 'react-native'; import { ActivityIndicator, TextInput, Button, HelperText, Checkbox, Text, } from 'react-native-paper'; import { useNavigation } from '@react-navigation/native'; import * as Location from 'expo-location'; import { showMessage } from 'react-native-flash-message'; import { validate } from 'validate.js'; import { useMutation } from '@apollo/client'; // TYPES/INTERFACES import { CreateInviteByLocationMutationArgsInterface } from '../../client/invite/argumentInterfaces'; // CONSTANTS import GROUPS from '../../router/groups.routes'; import { REQUIRE_NOT_EMPTY_PRESENCE } from '../../constants/rules.validate'; // MUTATIONS import { CREATE_INVITE_BY_LOCATION_MUTATION } from '../../client/invite/mutations'; // ACTIONS & SELECTORS import { useAppSelector, useAppDispatch } from '../../store/hooks'; import { onUserSignUpByAddressSuccess } from '../../store/features/user'; import { getLanguage } from '../../store/features/translation'; import { setGroup } from '../../store/features/navigation'; // HELPERS import { getFormattedLocation, FormattedLocationInterface, } from '../../helpers/location'; // COMPONENTS import { FocusAwareStatusBar, PaperText } from '../../components/Common'; // STYLES import { GLOBAL_STYLE as GS, CONSTANT_SIZE as CS, CONSTANT_COLOR as CC, } from '../../assets/ts/styles'; // TYPE export type FormInputNameType = 'city' | 'street' | 'house' | 'apartment'; export type FormType = { [name in FormInputNameType]: string; }; export type FormErrorsType = | { [name in FormInputNameType]: string[] | undefined; // tslint:disable-next-line: indent } | { [name: string]: string[] | undefined }; const SignUpByAddressScreen = () => { // SELECTORS const CURRENT_LANGUAGE = useAppSelector(getLanguage); // ACTIONS const reduxDispatch = useAppDispatch(); // NAVIGATION const NAVIGATION = useNavigation(); // STATES const [warningDialog, setWarningDialog] = React.useState<boolean>(false); const [form, setForm] = React.useState<FormType>({ city: '', street: '', house: '', apartment: '', }); const [formApartmentCheckbox, setFormApartmentCheckbox] = React.useState<boolean>(true); const [formErrors, setFormErrors] = React.useState<FormErrorsType>({}); const [canGoBack, setCanGoBack] = React.useState<boolean>(false); const [, /* preventBackCallBack */ setPreventBackCallBack] = React.useState< () => any >(() => {}); const [currentPosition, setCurrentPosition] = React.useState<Location.LocationObject | null>(null); const [formattedLocation, setFormattedLocation] = React.useState<FormattedLocationInterface | null>(null); const [addressLoading, setAddressLoading] = React.useState<boolean>(true); const [submitFormLoading, setSubmitFormLoading] = React.useState<boolean>(false); // DATA const STYLES = StyleSheet.create({ screen: { ...GS.screen, ...GS.bgSuccess, overflow: 'hidden', }, container: { ...GS.screen, ...GS.centered, ...GS.bgTransparent, ...GS.px5, ...GS.pb5, }, section1: { ...GS.centered, ...GS.pt5, ...GS.mt5, ...GS.pb3, ...GS.mb3, marginTop: CS.FONT_SIZE_LG * 3, }, section1Title: { ...GS.txtCenter, ...GS.mb3, ...GS.FF_NunitoBold, fontSize: CS.FONT_SIZE_LG * 1.8, }, section1SubTitle: { ...GS.txtCenter, ...GS.FF_NunitoBold, fontSize: CS.FONT_SIZE_MD, opacity: 0.6, }, section2: { ...GS.py2, ...GS.w100, alignItems: 'center' }, section2Title: { ...GS.txtCenter, ...GS.mb5, ...GS.FF_NunitoBold, fontSize: CS.FONT_SIZE_SM * 2, }, formContainer: { ...GS.w100, }, formInputContainer: {}, formInputContainerRow: { flex: 1 }, formInput: { ...GS.bgTransparent, ...GS.mb0, textAlign: 'center' }, formInputDisabled: { opacity: 0.4 }, formBtn: { ...GS.mb2 }, formBtnLabel: { ...GS.py1, ...GS.txtCapitalize, color: CC.light, fontSize: CS.FONT_SIZE + 3, }, formSubmitBtn: { ...(submitFormLoading ? { backgroundColor: CC.secondaryLight, // tslint:disable-next-line: indent } : GS.bgSecondary), }, formSkipBtn: { ...GS.bgLight }, formErrorHelperText: { textAlign: 'center', }, formErrorHelperTextApartment: { ...GS.mb2, marginTop: -(CS.SPACE - 5), }, }); // TODO: Add more constraints const VALIDATION_CONSTRAINT: { [name in FormInputNameType]?: object } = { city: REQUIRE_NOT_EMPTY_PRESENCE, street: REQUIRE_NOT_EMPTY_PRESENCE, house: REQUIRE_NOT_EMPTY_PRESENCE, apartment: REQUIRE_NOT_EMPTY_PRESENCE, }; // REFS const SCREEN_SCROLL_VIEW_REF = React.useRef<ScrollView | null>(null); const CITY_INPUT_REF = React.useRef<NativeTextInput | null>(null); const STREET_INPUT_REF = React.useRef<NativeTextInput | null>(null); const HOUSE_INPUT_REF = React.useRef<NativeTextInput | null>(null); const APARTMENT_INPUT_REF = React.useRef<NativeTextInput | null>(null); // MUTATIONS const [handleCreatInviteByLocation] = useMutation( CREATE_INVITE_BY_LOCATION_MUTATION, ); // FUNCTIONS const onSubmitForm = () => { setFormErrors({}); const FORMATTED_FORM = { ...form, ...formattedLocation, }; const FORMATTED_CONSTRAINTS = { ...VALIDATION_CONSTRAINT, }; if (!formApartmentCheckbox) { delete FORMATTED_CONSTRAINTS.apartment; } const VALIDATION_RESULT = validate( FORMATTED_FORM, FORMATTED_CONSTRAINTS, ); if (VALIDATION_RESULT) { setFormErrors(VALIDATION_RESULT); SCREEN_SCROLL_VIEW_REF?.current?.scrollTo({ y: 0 }); return; } setSubmitFormLoading(true); const CREATE_INVITE_INPUT: CreateInviteByLocationMutationArgsInterface = { createInput: { apartment: formApartmentCheckbox ? FORMATTED_FORM.apartment : '', geoLocation: { countryId: 0, city: FORMATTED_FORM.city, streetAddress: FORMATTED_FORM.streetAddress as string, house: FORMATTED_FORM.house, postcode: null, notes: null, loc: { type: 'Point', coordinates: [ FORMATTED_FORM.longitude as number, FORMATTED_FORM.latitude as number, ], }, }, }, }; handleCreatInviteByLocation({ variables: { ...CREATE_INVITE_INPUT, }, onCompleted: (TData) => { reduxDispatch(onUserSignUpByAddressSuccess(TData.createInvite)); reduxDispatch(setGroup(GROUPS.APP)); showMessage({ message: "Great job 🎉, you're sign-up as invite", type: 'success', }); setSubmitFormLoading(false); }, onError: (ApolloError) => { console.log('ApolloError ==>', ApolloError); showMessage({ message: ApolloError.name, description: ApolloError.message, type: 'danger', }); setSubmitFormLoading(false); }, }); }; // EFFECTS React.useEffect(() => { setAddressLoading(true); (async () => { const { status } = await Location.requestForegroundPermissionsAsync(); if (status !== 'granted') { const ERROR_MSG = 'Permission to access location was denied'; showMessage({ message: ERROR_MSG }); setCanGoBack(true); setTimeout(() => { NAVIGATION.goBack(); }, 100); return; } const CURRENT_POSITION = await Location.getCurrentPositionAsync({}); const FORMATTED_ADDRESS = await getFormattedLocation( CURRENT_POSITION.coords, ); setCurrentPosition(CURRENT_POSITION); setFormattedLocation(FORMATTED_ADDRESS); setAddressLoading(false); })(); }, [NAVIGATION]); React.useEffect(() => { // setCanGoBack(true); // reduxDispatch(setGroup(GROUPS.APP)); }, [reduxDispatch]); React.useEffect(() => { console.log( '\nLocation error ===> ', currentPosition, formattedLocation, ); }, [currentPosition, formattedLocation]); React.useEffect(() => { NAVIGATION.addListener('beforeRemove', (e) => { if (canGoBack) { return; } // Prevent default behavior of leaving the screen e.preventDefault(); setWarningDialog(true); // Prompt the user before leaving the screen setPreventBackCallBack(() => () => { setCanGoBack(true); setWarningDialog(false); NAVIGATION.dispatch(e.data.action); }); Alert.alert( 'Leave sign-up?', "Your account isn't yet created! Are you sure to leave the screen?", [ { text: "Don't leave", style: 'cancel', onPress: () => {} }, { text: 'leave', style: 'destructive', onPress: () => { setCanGoBack(true); NAVIGATION.dispatch(e.data.action); }, }, ], ); }); return () => NAVIGATION.removeListener('beforeRemove', () => null); }, [NAVIGATION, canGoBack, warningDialog]); return ( <ScrollView ref={SCREEN_SCROLL_VIEW_REF} style={{ ...GS.screenStatic }}> <FocusAwareStatusBar translucent={false} backgroundColor={CC.primary} barStyle='light-content' /> {/* Loading view */} <View style={STYLES.container}> {/* section1 */} <View style={STYLES.section1}> <PaperText style={STYLES.section1Title}> {CURRENT_LANGUAGE.INVITE_VIEW.YOUR_ADDRESS} </PaperText> <PaperText style={STYLES.section1SubTitle}> {CURRENT_LANGUAGE.INVITE_VIEW.LAUNCH_NOTIFICATION} </PaperText> </View> {/* section2 */} <View style={STYLES.section2}> {addressLoading ? ( <> <PaperText style={STYLES.section2Title}> { CURRENT_LANGUAGE.INVITE_VIEW .DETECTING_LOCATION } </PaperText> <ActivityIndicator color={CC.light} style={{ ...GS.mt5 }} /> </> ) : ( <View style={STYLES.formContainer}> <View style={STYLES.formInputContainer}> <TextInput ref={CITY_INPUT_REF} value={form.city} placeholder={CURRENT_LANGUAGE.CITY} autoComplete='street-address' textContentType='addressCity' keyboardType='default' style={STYLES.formInput} error={!!formErrors.city} mode='outlined' returnKeyLabel='next' returnKeyType='next' onSubmitEditing={() => STREET_INPUT_REF?.current?.focus() } onChangeText={(text) => setForm((prevForm) => ({ ...prevForm, city: text, })) } /> <HelperText visible={!!formErrors?.city} style={STYLES.formErrorHelperText} type='error'> {formErrors?.city ? formErrors.city[0] : ''} </HelperText> </View> <View style={STYLES.formInputContainer}> <TextInput ref={STREET_INPUT_REF} value={form.street} placeholder={CURRENT_LANGUAGE.STREET} autoComplete='street-address' textContentType='fullStreetAddress' keyboardType='default' style={STYLES.formInput} error={!!formErrors.street} mode='outlined' returnKeyLabel='next' returnKeyType='next' onSubmitEditing={() => HOUSE_INPUT_REF?.current?.focus() } onChangeText={(text) => setForm((prevForm) => ({ ...prevForm, street: text, })) } /> <HelperText visible={!!formErrors?.street} style={STYLES.formErrorHelperText} type='error'> {formErrors.street ? formErrors.street[0] : ''} </HelperText> </View> <View style={{ ...GS.row }}> <View style={{ ...STYLES.formInputContainer, ...STYLES.formInputContainerRow, }}> <TextInput ref={HOUSE_INPUT_REF} value={form.house} placeholder={CURRENT_LANGUAGE.HOUSE} keyboardType='default' style={{ ...STYLES.formInput, ...GS.mr2, }} error={!!formErrors.house} mode='outlined' returnKeyLabel='next' returnKeyType='next' onSubmitEditing={() => APARTMENT_INPUT_REF?.current?.focus() } onChangeText={(text) => setForm((prevForm) => ({ ...prevForm, house: text, })) } /> <HelperText visible={!!formErrors?.house} style={STYLES.formErrorHelperText} type='error'> {formErrors.house ? formErrors.house[0] : ''} </HelperText> </View> <View style={{ ...STYLES.formInputContainer, ...STYLES.formInputContainerRow, }}> <TextInput ref={APARTMENT_INPUT_REF} value={ formApartmentCheckbox ? form.apartment : '' } placeholder={CURRENT_LANGUAGE.APARTMENT} keyboardType='default' error={ !!formErrors.apartment && formApartmentCheckbox } disabled={!formApartmentCheckbox} editable={formApartmentCheckbox} style={{ ...STYLES.formInput, ...(!formApartmentCheckbox ? STYLES.formInputDisabled : {}), }} theme={{ colors: { primary: CC.secondary }, }} mode='outlined' returnKeyLabel='done' returnKeyType='done' onSubmitEditing={onSubmitForm} onChangeText={(text) => setForm((prevForm) => ({ ...prevForm, apartment: text, })) } /> <TouchableOpacity onPress={() => setFormApartmentCheckbox( !formApartmentCheckbox, ) } style={{ ...GS.justifyContentBetween, ...GS.mb0, }}> <PaperText> {CURRENT_LANGUAGE.APARTMENT} </PaperText> <Checkbox.Android status={ formApartmentCheckbox ? 'checked' : 'unchecked' } onPress={() => setFormApartmentCheckbox( !formApartmentCheckbox, ) } /> </TouchableOpacity> <HelperText visible={ !!formErrors?.apartment && formApartmentCheckbox } type='error' style={{ ...STYLES.formErrorHelperText, ...STYLES.formErrorHelperTextApartment, }}> {formErrors.apartment ? formErrors.apartment[0] : ''} </HelperText> </View> </View> <Button loading={submitFormLoading} disabled={submitFormLoading} uppercase={false} style={{ ...STYLES.formBtn, ...STYLES.formSubmitBtn, }} labelStyle={STYLES.formBtnLabel} theme={{ colors: { primary: CC.primary } }} onPress={onSubmitForm}> Submit </Button> <View style={{ ...GS.centered }}> <TouchableOpacity disabled={submitFormLoading}> <Text style={GS.mt2}> <Text style={GS.txtSecondary}> Click here </Text>{' '} to skip this step and fill these fields later </Text> </TouchableOpacity> </View> </View> )} </View> {/* TODO: find how to use a custom alert (disable due to slowing virtual device) */} {/* {warningDialog && ( <View style={{ ...GS.overlay, ...GS.centered, ...GS.p5 }}> <View style={{ ...GS.bgLight, ...GS.roundedMd, ...GS.w100, ...GS.shadow, ...GS.py4, ...GS.px2, }}> <NativeTxt style={{ ...GS.mb4, fontSize: CS.FONT_SIZE_LG, ...GS.txtPrimary, }}> Leave? </NativeTxt> <View style={{ ...GS.mb2 }}> <NativeBtn title='Leave' color={CC.danger} onPress={() => preventBackCallBack()} /> </View> <NativeBtn title="Don't leave" onPress={() => setWarningDialog(false)} /> </View> </View> )} */} </View> </ScrollView> ); }; export default SignUpByAddressScreen; Loading
packages/shop-mobile-expo/src/screens/authentication/Home.screen.tsx 0 → 100644 +149 −0 Original line number Diff line number Diff line import React from 'react'; import { Text, View, Image, StyleSheet } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { Button } from 'react-native-paper'; // tslint:disable-next-line: no-implicit-dependencies no-submodule-imports import AntDesignIcon from '@expo/vector-icons/AntDesign'; // ACTIONS & SELECTORS import { useAppSelector } from '../../store/hooks'; import { getLanguage } from '../../store/features/translation'; // COMPONENTS import { FocusAwareStatusBar, PaperText } from '../../components/Common'; // STYLES import { GLOBAL_STYLE as GS, CONSTANT_SIZE as CS, CONSTANT_COLOR as CC, } from '../../assets/ts/styles'; const STYLES = StyleSheet.create({ container: { ...GS.screen, ...GS.bgTransparent, ...GS.px5, ...GS.pb5, ...GS.mx5, zIndex: 1, }, titleLogoContainer: { ...GS.centered, flex: 1, }, logoImg: { ...GS.w100, height: 100, marginBottom: -20 }, logoTitle: { ...GS.txtCapitalize, fontSize: CS.FONT_SIZE + 1, opacity: 0.7, }, networkBtnFacebook: { flex: 1, backgroundColor: CC.facebook }, networkBtnGoogle: { flex: 1, backgroundColor: CC.google }, }); const HomeAuthScreen = () => { // SELECTORS const currentLanguage = useAppSelector(getLanguage); // NAVIGATION const navigation = useNavigation(); // FUNCTIONS const onPressSignUpByAddress = () => { navigation.navigate('STACK/SIGN_UP_BY_ADDRESS' as never); }; return ( <View style={{ ...GS.screen }}> <FocusAwareStatusBar translucent={false} backgroundColor={CC.primary} barStyle='light-content' /> <View style={STYLES.container}> {/* title logo */} <View style={STYLES.titleLogoContainer}> <Image source={require('../../assets/img/ever/logo.png')} resizeMode='contain' style={STYLES.logoImg} /> <Text style={STYLES.logoTitle}> {currentLanguage.INVITE_VIEW.BY_CODE.LOGO.DETAILS} </Text> </View> {/* Social Networks buttons */} <View style={{ ...GS.py4 }}> <View> <Button mode='contained' style={{ ...GS.bgSecondary, ...GS.mb2, }} labelStyle={{ ...GS.txtCapitalize, ...GS.py1 }} onPress={() => onPressSignUpByAddress()}> {currentLanguage.INVITE_VIEW.GET_IN_BY_ADDRESS} </Button> <View style={{ ...GS.inlineItems, ...GS.centered }}> <Button mode='contained' style={STYLES.networkBtnFacebook}> <AntDesignIcon name='facebook-square' color={CC.light} size={CS.FONT_SIZE * 2} /> </Button> <View style={{ ...GS.mr2 }} /> <Button mode='contained' style={STYLES.networkBtnGoogle}> <AntDesignIcon name='google' color={CC.light} size={CS.FONT_SIZE * 2} /> </Button> </View> </View> <View style={{ ...GS.py4, ...GS.my3, ...GS.centered }}> <PaperText style={{ ...GS.txtLower, color: CC.gray, fontSize: CS.FONT_SIZE + 3, }}> {currentLanguage.OR}{' '} <Text style={{ ...GS.fontBold, color: CC.light }}> {currentLanguage.INVITE_VIEW.BY_CODE.OR_WHAT} </Text> </PaperText> </View> <View> <Button mode='outlined' style={{ ...GS.py1, borderColor: CC.gray }} labelStyle={{ ...GS.txtCapitalize, color: CC.gray, fontSize: CS.FONT_SIZE + 3, }}> {currentLanguage.INVITE_VIEW.BY_CODE.INVITE_CODE} </Button> </View> </View> </View> </View> ); }; export default HomeAuthScreen;
packages/shop-mobile-expo/src/screens/authentication/SignIn.screen.tsx 0 → 100644 +12 −0 Original line number Diff line number Diff line import React from 'react'; import { Text, View } from 'react-native'; export interface Props {} const SignInScreen: React.FC<Props> = ({}) => ( <View> <Text>SignInScreen</Text> </View> ); export default SignInScreen;
packages/shop-mobile-expo/src/screens/authentication/SignUp.screen.tsx 0 → 100644 +12 −0 Original line number Diff line number Diff line import React from 'react'; import { Text, View } from 'react-native'; export interface Props {} const SignUpScreen: React.FC<Props> = () => ( <View> <Text>SignUpScreen</Text> </View> ); export default SignUpScreen;
packages/shop-mobile-expo/src/screens/authentication/SignUpByAddress.screen.tsx 0 → 100644 +650 −0 Original line number Diff line number Diff line import React from 'react'; import { View, StyleSheet, ScrollView, Alert, TouchableOpacity, // Button as NativeBtn, TextInput as NativeTextInput, } from 'react-native'; import { ActivityIndicator, TextInput, Button, HelperText, Checkbox, Text, } from 'react-native-paper'; import { useNavigation } from '@react-navigation/native'; import * as Location from 'expo-location'; import { showMessage } from 'react-native-flash-message'; import { validate } from 'validate.js'; import { useMutation } from '@apollo/client'; // TYPES/INTERFACES import { CreateInviteByLocationMutationArgsInterface } from '../../client/invite/argumentInterfaces'; // CONSTANTS import GROUPS from '../../router/groups.routes'; import { REQUIRE_NOT_EMPTY_PRESENCE } from '../../constants/rules.validate'; // MUTATIONS import { CREATE_INVITE_BY_LOCATION_MUTATION } from '../../client/invite/mutations'; // ACTIONS & SELECTORS import { useAppSelector, useAppDispatch } from '../../store/hooks'; import { onUserSignUpByAddressSuccess } from '../../store/features/user'; import { getLanguage } from '../../store/features/translation'; import { setGroup } from '../../store/features/navigation'; // HELPERS import { getFormattedLocation, FormattedLocationInterface, } from '../../helpers/location'; // COMPONENTS import { FocusAwareStatusBar, PaperText } from '../../components/Common'; // STYLES import { GLOBAL_STYLE as GS, CONSTANT_SIZE as CS, CONSTANT_COLOR as CC, } from '../../assets/ts/styles'; // TYPE export type FormInputNameType = 'city' | 'street' | 'house' | 'apartment'; export type FormType = { [name in FormInputNameType]: string; }; export type FormErrorsType = | { [name in FormInputNameType]: string[] | undefined; // tslint:disable-next-line: indent } | { [name: string]: string[] | undefined }; const SignUpByAddressScreen = () => { // SELECTORS const CURRENT_LANGUAGE = useAppSelector(getLanguage); // ACTIONS const reduxDispatch = useAppDispatch(); // NAVIGATION const NAVIGATION = useNavigation(); // STATES const [warningDialog, setWarningDialog] = React.useState<boolean>(false); const [form, setForm] = React.useState<FormType>({ city: '', street: '', house: '', apartment: '', }); const [formApartmentCheckbox, setFormApartmentCheckbox] = React.useState<boolean>(true); const [formErrors, setFormErrors] = React.useState<FormErrorsType>({}); const [canGoBack, setCanGoBack] = React.useState<boolean>(false); const [, /* preventBackCallBack */ setPreventBackCallBack] = React.useState< () => any >(() => {}); const [currentPosition, setCurrentPosition] = React.useState<Location.LocationObject | null>(null); const [formattedLocation, setFormattedLocation] = React.useState<FormattedLocationInterface | null>(null); const [addressLoading, setAddressLoading] = React.useState<boolean>(true); const [submitFormLoading, setSubmitFormLoading] = React.useState<boolean>(false); // DATA const STYLES = StyleSheet.create({ screen: { ...GS.screen, ...GS.bgSuccess, overflow: 'hidden', }, container: { ...GS.screen, ...GS.centered, ...GS.bgTransparent, ...GS.px5, ...GS.pb5, }, section1: { ...GS.centered, ...GS.pt5, ...GS.mt5, ...GS.pb3, ...GS.mb3, marginTop: CS.FONT_SIZE_LG * 3, }, section1Title: { ...GS.txtCenter, ...GS.mb3, ...GS.FF_NunitoBold, fontSize: CS.FONT_SIZE_LG * 1.8, }, section1SubTitle: { ...GS.txtCenter, ...GS.FF_NunitoBold, fontSize: CS.FONT_SIZE_MD, opacity: 0.6, }, section2: { ...GS.py2, ...GS.w100, alignItems: 'center' }, section2Title: { ...GS.txtCenter, ...GS.mb5, ...GS.FF_NunitoBold, fontSize: CS.FONT_SIZE_SM * 2, }, formContainer: { ...GS.w100, }, formInputContainer: {}, formInputContainerRow: { flex: 1 }, formInput: { ...GS.bgTransparent, ...GS.mb0, textAlign: 'center' }, formInputDisabled: { opacity: 0.4 }, formBtn: { ...GS.mb2 }, formBtnLabel: { ...GS.py1, ...GS.txtCapitalize, color: CC.light, fontSize: CS.FONT_SIZE + 3, }, formSubmitBtn: { ...(submitFormLoading ? { backgroundColor: CC.secondaryLight, // tslint:disable-next-line: indent } : GS.bgSecondary), }, formSkipBtn: { ...GS.bgLight }, formErrorHelperText: { textAlign: 'center', }, formErrorHelperTextApartment: { ...GS.mb2, marginTop: -(CS.SPACE - 5), }, }); // TODO: Add more constraints const VALIDATION_CONSTRAINT: { [name in FormInputNameType]?: object } = { city: REQUIRE_NOT_EMPTY_PRESENCE, street: REQUIRE_NOT_EMPTY_PRESENCE, house: REQUIRE_NOT_EMPTY_PRESENCE, apartment: REQUIRE_NOT_EMPTY_PRESENCE, }; // REFS const SCREEN_SCROLL_VIEW_REF = React.useRef<ScrollView | null>(null); const CITY_INPUT_REF = React.useRef<NativeTextInput | null>(null); const STREET_INPUT_REF = React.useRef<NativeTextInput | null>(null); const HOUSE_INPUT_REF = React.useRef<NativeTextInput | null>(null); const APARTMENT_INPUT_REF = React.useRef<NativeTextInput | null>(null); // MUTATIONS const [handleCreatInviteByLocation] = useMutation( CREATE_INVITE_BY_LOCATION_MUTATION, ); // FUNCTIONS const onSubmitForm = () => { setFormErrors({}); const FORMATTED_FORM = { ...form, ...formattedLocation, }; const FORMATTED_CONSTRAINTS = { ...VALIDATION_CONSTRAINT, }; if (!formApartmentCheckbox) { delete FORMATTED_CONSTRAINTS.apartment; } const VALIDATION_RESULT = validate( FORMATTED_FORM, FORMATTED_CONSTRAINTS, ); if (VALIDATION_RESULT) { setFormErrors(VALIDATION_RESULT); SCREEN_SCROLL_VIEW_REF?.current?.scrollTo({ y: 0 }); return; } setSubmitFormLoading(true); const CREATE_INVITE_INPUT: CreateInviteByLocationMutationArgsInterface = { createInput: { apartment: formApartmentCheckbox ? FORMATTED_FORM.apartment : '', geoLocation: { countryId: 0, city: FORMATTED_FORM.city, streetAddress: FORMATTED_FORM.streetAddress as string, house: FORMATTED_FORM.house, postcode: null, notes: null, loc: { type: 'Point', coordinates: [ FORMATTED_FORM.longitude as number, FORMATTED_FORM.latitude as number, ], }, }, }, }; handleCreatInviteByLocation({ variables: { ...CREATE_INVITE_INPUT, }, onCompleted: (TData) => { reduxDispatch(onUserSignUpByAddressSuccess(TData.createInvite)); reduxDispatch(setGroup(GROUPS.APP)); showMessage({ message: "Great job 🎉, you're sign-up as invite", type: 'success', }); setSubmitFormLoading(false); }, onError: (ApolloError) => { console.log('ApolloError ==>', ApolloError); showMessage({ message: ApolloError.name, description: ApolloError.message, type: 'danger', }); setSubmitFormLoading(false); }, }); }; // EFFECTS React.useEffect(() => { setAddressLoading(true); (async () => { const { status } = await Location.requestForegroundPermissionsAsync(); if (status !== 'granted') { const ERROR_MSG = 'Permission to access location was denied'; showMessage({ message: ERROR_MSG }); setCanGoBack(true); setTimeout(() => { NAVIGATION.goBack(); }, 100); return; } const CURRENT_POSITION = await Location.getCurrentPositionAsync({}); const FORMATTED_ADDRESS = await getFormattedLocation( CURRENT_POSITION.coords, ); setCurrentPosition(CURRENT_POSITION); setFormattedLocation(FORMATTED_ADDRESS); setAddressLoading(false); })(); }, [NAVIGATION]); React.useEffect(() => { // setCanGoBack(true); // reduxDispatch(setGroup(GROUPS.APP)); }, [reduxDispatch]); React.useEffect(() => { console.log( '\nLocation error ===> ', currentPosition, formattedLocation, ); }, [currentPosition, formattedLocation]); React.useEffect(() => { NAVIGATION.addListener('beforeRemove', (e) => { if (canGoBack) { return; } // Prevent default behavior of leaving the screen e.preventDefault(); setWarningDialog(true); // Prompt the user before leaving the screen setPreventBackCallBack(() => () => { setCanGoBack(true); setWarningDialog(false); NAVIGATION.dispatch(e.data.action); }); Alert.alert( 'Leave sign-up?', "Your account isn't yet created! Are you sure to leave the screen?", [ { text: "Don't leave", style: 'cancel', onPress: () => {} }, { text: 'leave', style: 'destructive', onPress: () => { setCanGoBack(true); NAVIGATION.dispatch(e.data.action); }, }, ], ); }); return () => NAVIGATION.removeListener('beforeRemove', () => null); }, [NAVIGATION, canGoBack, warningDialog]); return ( <ScrollView ref={SCREEN_SCROLL_VIEW_REF} style={{ ...GS.screenStatic }}> <FocusAwareStatusBar translucent={false} backgroundColor={CC.primary} barStyle='light-content' /> {/* Loading view */} <View style={STYLES.container}> {/* section1 */} <View style={STYLES.section1}> <PaperText style={STYLES.section1Title}> {CURRENT_LANGUAGE.INVITE_VIEW.YOUR_ADDRESS} </PaperText> <PaperText style={STYLES.section1SubTitle}> {CURRENT_LANGUAGE.INVITE_VIEW.LAUNCH_NOTIFICATION} </PaperText> </View> {/* section2 */} <View style={STYLES.section2}> {addressLoading ? ( <> <PaperText style={STYLES.section2Title}> { CURRENT_LANGUAGE.INVITE_VIEW .DETECTING_LOCATION } </PaperText> <ActivityIndicator color={CC.light} style={{ ...GS.mt5 }} /> </> ) : ( <View style={STYLES.formContainer}> <View style={STYLES.formInputContainer}> <TextInput ref={CITY_INPUT_REF} value={form.city} placeholder={CURRENT_LANGUAGE.CITY} autoComplete='street-address' textContentType='addressCity' keyboardType='default' style={STYLES.formInput} error={!!formErrors.city} mode='outlined' returnKeyLabel='next' returnKeyType='next' onSubmitEditing={() => STREET_INPUT_REF?.current?.focus() } onChangeText={(text) => setForm((prevForm) => ({ ...prevForm, city: text, })) } /> <HelperText visible={!!formErrors?.city} style={STYLES.formErrorHelperText} type='error'> {formErrors?.city ? formErrors.city[0] : ''} </HelperText> </View> <View style={STYLES.formInputContainer}> <TextInput ref={STREET_INPUT_REF} value={form.street} placeholder={CURRENT_LANGUAGE.STREET} autoComplete='street-address' textContentType='fullStreetAddress' keyboardType='default' style={STYLES.formInput} error={!!formErrors.street} mode='outlined' returnKeyLabel='next' returnKeyType='next' onSubmitEditing={() => HOUSE_INPUT_REF?.current?.focus() } onChangeText={(text) => setForm((prevForm) => ({ ...prevForm, street: text, })) } /> <HelperText visible={!!formErrors?.street} style={STYLES.formErrorHelperText} type='error'> {formErrors.street ? formErrors.street[0] : ''} </HelperText> </View> <View style={{ ...GS.row }}> <View style={{ ...STYLES.formInputContainer, ...STYLES.formInputContainerRow, }}> <TextInput ref={HOUSE_INPUT_REF} value={form.house} placeholder={CURRENT_LANGUAGE.HOUSE} keyboardType='default' style={{ ...STYLES.formInput, ...GS.mr2, }} error={!!formErrors.house} mode='outlined' returnKeyLabel='next' returnKeyType='next' onSubmitEditing={() => APARTMENT_INPUT_REF?.current?.focus() } onChangeText={(text) => setForm((prevForm) => ({ ...prevForm, house: text, })) } /> <HelperText visible={!!formErrors?.house} style={STYLES.formErrorHelperText} type='error'> {formErrors.house ? formErrors.house[0] : ''} </HelperText> </View> <View style={{ ...STYLES.formInputContainer, ...STYLES.formInputContainerRow, }}> <TextInput ref={APARTMENT_INPUT_REF} value={ formApartmentCheckbox ? form.apartment : '' } placeholder={CURRENT_LANGUAGE.APARTMENT} keyboardType='default' error={ !!formErrors.apartment && formApartmentCheckbox } disabled={!formApartmentCheckbox} editable={formApartmentCheckbox} style={{ ...STYLES.formInput, ...(!formApartmentCheckbox ? STYLES.formInputDisabled : {}), }} theme={{ colors: { primary: CC.secondary }, }} mode='outlined' returnKeyLabel='done' returnKeyType='done' onSubmitEditing={onSubmitForm} onChangeText={(text) => setForm((prevForm) => ({ ...prevForm, apartment: text, })) } /> <TouchableOpacity onPress={() => setFormApartmentCheckbox( !formApartmentCheckbox, ) } style={{ ...GS.justifyContentBetween, ...GS.mb0, }}> <PaperText> {CURRENT_LANGUAGE.APARTMENT} </PaperText> <Checkbox.Android status={ formApartmentCheckbox ? 'checked' : 'unchecked' } onPress={() => setFormApartmentCheckbox( !formApartmentCheckbox, ) } /> </TouchableOpacity> <HelperText visible={ !!formErrors?.apartment && formApartmentCheckbox } type='error' style={{ ...STYLES.formErrorHelperText, ...STYLES.formErrorHelperTextApartment, }}> {formErrors.apartment ? formErrors.apartment[0] : ''} </HelperText> </View> </View> <Button loading={submitFormLoading} disabled={submitFormLoading} uppercase={false} style={{ ...STYLES.formBtn, ...STYLES.formSubmitBtn, }} labelStyle={STYLES.formBtnLabel} theme={{ colors: { primary: CC.primary } }} onPress={onSubmitForm}> Submit </Button> <View style={{ ...GS.centered }}> <TouchableOpacity disabled={submitFormLoading}> <Text style={GS.mt2}> <Text style={GS.txtSecondary}> Click here </Text>{' '} to skip this step and fill these fields later </Text> </TouchableOpacity> </View> </View> )} </View> {/* TODO: find how to use a custom alert (disable due to slowing virtual device) */} {/* {warningDialog && ( <View style={{ ...GS.overlay, ...GS.centered, ...GS.p5 }}> <View style={{ ...GS.bgLight, ...GS.roundedMd, ...GS.w100, ...GS.shadow, ...GS.py4, ...GS.px2, }}> <NativeTxt style={{ ...GS.mb4, fontSize: CS.FONT_SIZE_LG, ...GS.txtPrimary, }}> Leave? </NativeTxt> <View style={{ ...GS.mb2 }}> <NativeBtn title='Leave' color={CC.danger} onPress={() => preventBackCallBack()} /> </View> <NativeBtn title="Don't leave" onPress={() => setWarningDialog(false)} /> </View> </View> )} */} </View> </ScrollView> ); }; export default SignUpByAddressScreen;