Skip to content

Commit

Permalink
Merge pull request #29983 from software-mansion-labs/form-migration/a…
Browse files Browse the repository at this point in the history
…dd-debit-card-page

[Form Provider Refactor] AddDebitCardPage
  • Loading branch information
luacmartins authored Oct 30, 2023
2 parents c54cbdd + 890dd54 commit 052fd0b
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 80 deletions.
150 changes: 88 additions & 62 deletions src/components/AddressSearch/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React, {useEffect, useMemo, useRef, useState} from 'react';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {ActivityIndicator, Keyboard, LogBox, ScrollView, Text, View} from 'react-native';
import {GooglePlacesAutocomplete} from 'react-native-google-places-autocomplete';
import _ from 'underscore';
Expand Down Expand Up @@ -140,27 +140,46 @@ const defaultProps = {
resultTypes: 'address',
};

// Do not convert to class component! It's been tried before and presents more challenges than it's worth.
// Relevant thread: https://expensify.slack.com/archives/C03TQ48KC/p1634088400387400
// Reference: https://github.com/FaridSafi/react-native-google-places-autocomplete/issues/609#issuecomment-886133839
function AddressSearch(props) {
function AddressSearch({
canUseCurrentLocation,
containerStyles,
defaultValue,
errorText,
hint,
innerRef,
inputID,
isLimitedToUSA,
label,
maxInputLength,
network,
onBlur,
onInputChange,
onPress,
predefinedPlaces,
preferredLocale,
renamedInputKeys,
resultTypes,
shouldSaveDraft,
translate,
value,
}) {
const [displayListViewBorder, setDisplayListViewBorder] = useState(false);
const [isTyping, setIsTyping] = useState(false);
const [isFocused, setIsFocused] = useState(false);
const [searchValue, setSearchValue] = useState(props.value || props.defaultValue || '');
const [searchValue, setSearchValue] = useState(value || defaultValue || '');
const [locationErrorCode, setLocationErrorCode] = useState(null);
const [isFetchingCurrentLocation, setIsFetchingCurrentLocation] = useState(false);
const shouldTriggerGeolocationCallbacks = useRef(true);
const containerRef = useRef();
const query = useMemo(
() => ({
language: props.preferredLocale,
types: props.resultTypes,
components: props.isLimitedToUSA ? 'country:us' : undefined,
language: preferredLocale,
types: resultTypes,
components: isLimitedToUSA ? 'country:us' : undefined,
}),
[props.preferredLocale, props.resultTypes, props.isLimitedToUSA],
[preferredLocale, resultTypes, isLimitedToUSA],
);
const shouldShowCurrentLocationButton = props.canUseCurrentLocation && searchValue.trim().length === 0 && isFocused;
const shouldShowCurrentLocationButton = canUseCurrentLocation && searchValue.trim().length === 0 && isFocused;

const saveLocationDetails = (autocompleteData, details) => {
const addressComponents = details.address_components;
Expand All @@ -169,7 +188,7 @@ function AddressSearch(props) {
// to this component which don't match the usual properties coming from auto-complete. In that case, only a limited
// amount of data massaging needs to happen for what the parent expects to get from this function.
if (_.size(details)) {
props.onPress({
onPress({
address: lodashGet(details, 'description'),
lat: lodashGet(details, 'geometry.location.lat', 0),
lng: lodashGet(details, 'geometry.location.lng', 0),
Expand Down Expand Up @@ -256,7 +275,7 @@ function AddressSearch(props) {

// Not all pages define the Address Line 2 field, so in that case we append any additional address details
// (e.g. Apt #) to Address Line 1
if (subpremise && typeof props.renamedInputKeys.street2 === 'undefined') {
if (subpremise && typeof renamedInputKeys.street2 === 'undefined') {
values.street += `, ${subpremise}`;
}

Expand All @@ -265,19 +284,19 @@ function AddressSearch(props) {
values.country = country;
}

if (props.inputID) {
_.each(values, (value, key) => {
const inputKey = lodashGet(props.renamedInputKeys, key, key);
if (inputID) {
_.each(values, (inputValue, key) => {
const inputKey = lodashGet(renamedInputKeys, key, key);
if (!inputKey) {
return;
}
props.onInputChange(value, inputKey);
onInputChange(inputValue, inputKey);
});
} else {
props.onInputChange(values);
onInputChange(values);
}

props.onPress(values);
onPress(values);
};

/** Gets the user's current location and registers success/error callbacks */
Expand Down Expand Up @@ -307,7 +326,7 @@ function AddressSearch(props) {
lng: successData.coords.longitude,
address: CONST.YOUR_LOCATION_TEXT,
};
props.onPress(location);
onPress(location);
},
(errorData) => {
if (!shouldTriggerGeolocationCallbacks.current) {
Expand All @@ -325,16 +344,16 @@ function AddressSearch(props) {
};

const renderHeaderComponent = () =>
props.predefinedPlaces.length > 0 && (
predefinedPlaces.length > 0 && (
<>
{/* This will show current location button in list if there are some recent destinations */}
{shouldShowCurrentLocationButton && (
<CurrentLocationButton
onPress={getCurrentLocation}
isDisabled={props.network.isOffline}
isDisabled={network.isOffline}
/>
)}
{!props.value && <Text style={[styles.textLabel, styles.colorMuted, styles.pv2, styles.ph3, styles.overflowAuto]}>{props.translate('common.recentDestinations')}</Text>}
{!value && <Text style={[styles.textLabel, styles.colorMuted, styles.pv2, styles.ph3, styles.overflowAuto]}>{translate('common.recentDestinations')}</Text>}
</>
);

Expand All @@ -346,6 +365,26 @@ function AddressSearch(props) {
};
}, []);

const listEmptyComponent = useCallback(
() =>
network.isOffline || !isTyping ? null : (
<Text style={[styles.textLabel, styles.colorMuted, styles.pv4, styles.ph3, styles.overflowAuto]}>{translate('common.noResultsFound')}</Text>
),
[isTyping, translate, network.isOffline],
);

const listLoader = useCallback(
() => (
<View style={[styles.pv4]}>
<ActivityIndicator
color={themeColors.spinner}
size="small"
/>
</View>
),
[],
);

return (
/*
* The GooglePlacesAutocomplete component uses a VirtualizedList internally,
Expand All @@ -372,20 +411,10 @@ function AddressSearch(props) {
fetchDetails
suppressDefaultStyles
enablePoweredByContainer={false}
predefinedPlaces={props.predefinedPlaces}
listEmptyComponent={
props.network.isOffline || !isTyping ? null : (
<Text style={[styles.textLabel, styles.colorMuted, styles.pv4, styles.ph3, styles.overflowAuto]}>{props.translate('common.noResultsFound')}</Text>
)
}
listLoaderComponent={
<View style={[styles.pv4]}>
<ActivityIndicator
color={themeColors.spinner}
size="small"
/>
</View>
}
predefinedPlaces={predefinedPlaces}
listEmptyComponent={listEmptyComponent}
listLoaderComponent={listLoader}
renderHeaderComponent={renderHeaderComponent}
renderRow={(data) => {
const title = data.isPredefinedPlace ? data.name : data.structured_formatting.main_text;
const subtitle = data.isPredefinedPlace ? data.description : data.structured_formatting.secondary_text;
Expand All @@ -396,7 +425,6 @@ function AddressSearch(props) {
</View>
);
}}
renderHeaderComponent={renderHeaderComponent}
onPress={(data, details) => {
saveLocationDetails(data, details);
setIsTyping(false);
Expand All @@ -411,34 +439,31 @@ function AddressSearch(props) {
query={query}
requestUrl={{
useOnPlatform: 'all',
url: props.network.isOffline ? null : ApiUtils.getCommandURL({command: 'Proxy_GooglePlaces&proxyUrl='}),
url: network.isOffline ? null : ApiUtils.getCommandURL({command: 'Proxy_GooglePlaces&proxyUrl='}),
}}
textInputProps={{
InputComp: TextInput,
ref: (node) => {
if (!props.innerRef) {
if (!innerRef) {
return;
}

if (_.isFunction(props.innerRef)) {
props.innerRef(node);
if (_.isFunction(innerRef)) {
innerRef(node);
return;
}

// eslint-disable-next-line no-param-reassign
props.innerRef.current = node;
innerRef.current = node;
},
label: props.label,
containerStyles: props.containerStyles,
errorText: props.errorText,
hint:
displayListViewBorder || (props.predefinedPlaces.length === 0 && shouldShowCurrentLocationButton) || (props.canUseCurrentLocation && isTyping)
? undefined
: props.hint,
value: props.value,
defaultValue: props.defaultValue,
inputID: props.inputID,
shouldSaveDraft: props.shouldSaveDraft,
label,
containerStyles,
errorText,
hint: displayListViewBorder || (predefinedPlaces.length === 0 && shouldShowCurrentLocationButton) || (canUseCurrentLocation && isTyping) ? undefined : hint,
value,
defaultValue,
inputID,
shouldSaveDraft,
onFocus: () => {
setIsFocused(true);
},
Expand All @@ -448,24 +473,24 @@ function AddressSearch(props) {
setIsFocused(false);
setIsTyping(false);
}
props.onBlur();
onBlur();
},
autoComplete: 'off',
onInputChange: (text) => {
setSearchValue(text);
setIsTyping(true);
if (props.inputID) {
props.onInputChange(text);
if (inputID) {
onInputChange(text);
} else {
props.onInputChange({street: text});
onInputChange({street: text});
}

// If the text is empty and we have no predefined places, we set displayListViewBorder to false to prevent UI flickering
if (_.isEmpty(text) && _.isEmpty(props.predefinedPlaces)) {
if (_.isEmpty(text) && _.isEmpty(predefinedPlaces)) {
setDisplayListViewBorder(false);
}
},
maxLength: props.maxInputLength,
maxLength: maxInputLength,
spellCheck: false,
}}
styles={{
Expand All @@ -486,17 +511,18 @@ function AddressSearch(props) {
}}
inbetweenCompo={
// We want to show the current location button even if there are no recent destinations
props.predefinedPlaces.length === 0 && shouldShowCurrentLocationButton ? (
predefinedPlaces.length === 0 && shouldShowCurrentLocationButton ? (
<View style={[StyleUtils.getGoogleListViewStyle(true), styles.overflowAuto, styles.borderLeft, styles.borderRight]}>
<CurrentLocationButton
onPress={getCurrentLocation}
isDisabled={props.network.isOffline}
isDisabled={network.isOffline}
/>
</View>
) : (
<></>
)
}
placeholder=""
/>
<LocationErrorMessage
onClose={() => setLocationErrorCode(null)}
Expand Down
3 changes: 2 additions & 1 deletion src/components/CheckboxWithLabel.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import variables from '@styles/variables';
import Checkbox from './Checkbox';
import FormHelpMessage from './FormHelpMessage';
import PressableWithFeedback from './Pressable/PressableWithFeedback';
import refPropTypes from './refPropTypes';
import Text from './Text';

/**
Expand Down Expand Up @@ -54,7 +55,7 @@ const propTypes = {
defaultValue: PropTypes.bool,

/** React ref being forwarded to the Checkbox input */
forwardedRef: PropTypes.func,
forwardedRef: refPropTypes,

/** The ID used to uniquely identify the input in a Form */
/* eslint-disable-next-line react/no-unused-prop-types */
Expand Down
21 changes: 16 additions & 5 deletions src/components/Form/FormProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ const propTypes = {
shouldValidateOnChange: PropTypes.bool,
};

const VALIDATE_DELAY = 200;

const defaultProps = {
isSubmitButtonVisible: true,
formState: {
Expand Down Expand Up @@ -246,19 +248,28 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC
// as this is already happening by the value prop.
defaultValue: undefined,
onTouched: (event) => {
setTouchedInput(inputID);
setTimeout(() => {
setTouchedInput(inputID);
}, VALIDATE_DELAY);
if (_.isFunction(propsToParse.onTouched)) {
propsToParse.onTouched(event);
}
},
onPress: (event) => {
setTouchedInput(inputID);
setTimeout(() => {
setTouchedInput(inputID);
}, VALIDATE_DELAY);
if (_.isFunction(propsToParse.onPress)) {
propsToParse.onPress(event);
}
},
onPressIn: (event) => {
setTouchedInput(inputID);
onPressOut: (event) => {
// To prevent validating just pressed inputs, we need to set the touched input right after
// onValidate and to do so, we need to delays setTouchedInput of the same amount of time
// as the onValidate is delayed
setTimeout(() => {
setTouchedInput(inputID);
}, VALIDATE_DELAY);
if (_.isFunction(propsToParse.onPressIn)) {
propsToParse.onPressIn(event);
}
Expand All @@ -274,7 +285,7 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC
if (shouldValidateOnBlur) {
onValidate(inputValues, !hasServerError);
}
}, 200);
}, VALIDATE_DELAY);
}

if (_.isFunction(propsToParse.onBlur)) {
Expand Down
Loading

0 comments on commit 052fd0b

Please sign in to comment.