-
Notifications
You must be signed in to change notification settings - Fork 2.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(expensify-card): add get physical card button and routes #28453
Changes from all commits
a147db2
ed99be3
2f21fbb
0f29db9
2096411
88e280b
5c382ee
3993850
3ea88a7
014f0bc
6530e75
4ff162d
53afcf3
be37b21
d80f3d5
63a4de9
28a309c
53bfa83
5f5f158
c69825c
2d61f54
8bbf4ab
6e28feb
d6b9d52
d3772fe
f51068d
1290c7c
456c7c9
f777b38
a9559f4
d6b86f7
db37d3b
67449d7
d2576bb
2d09ef7
bbaea03
16f8494
26f0ac7
314c7b7
a9f18aa
8d11d57
40acd98
218f194
6fe8995
47ad65e
3172f9a
8bb38f4
e600b84
f213505
41b7976
e831853
57cf5e5
d0ea79a
1054b16
56554f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,223 @@ | ||
import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; | ||
import lodashGet from 'lodash/get'; | ||
import PropTypes from 'prop-types'; | ||
import React, {useCallback} from 'react'; | ||
import {View} from 'react-native'; | ||
import _ from 'underscore'; | ||
import useLocalize from '@hooks/useLocalize'; | ||
import Navigation from '@libs/Navigation/Navigation'; | ||
import * as ValidationUtils from '@libs/ValidationUtils'; | ||
import styles from '@styles/styles'; | ||
import CONST from '@src/CONST'; | ||
import AddressSearch from './AddressSearch'; | ||
import CountrySelector from './CountrySelector'; | ||
import Form from './Form'; | ||
import StatePicker from './StatePicker'; | ||
import TextInput from './TextInput'; | ||
|
||
const propTypes = { | ||
/** Address city field */ | ||
city: PropTypes.string, | ||
|
||
/** Address country field */ | ||
country: PropTypes.string, | ||
|
||
/** Address state field */ | ||
state: PropTypes.string, | ||
|
||
/** Address street line 1 field */ | ||
street1: PropTypes.string, | ||
|
||
/** Address street line 2 field */ | ||
street2: PropTypes.string, | ||
|
||
/** Address zip code field */ | ||
zip: PropTypes.string, | ||
|
||
/** Callback which is executed when the user changes address, city or state */ | ||
onAddressChanged: PropTypes.func, | ||
|
||
/** Callback which is executed when the user submits his address changes */ | ||
onSubmit: PropTypes.func.isRequired, | ||
|
||
/** Whether or not should the form data should be saved as draft */ | ||
shouldSaveDraft: PropTypes.bool, | ||
|
||
/** Text displayed on the bottom submit button */ | ||
submitButtonText: PropTypes.string, | ||
|
||
/** A unique Onyx key identifying the form */ | ||
formID: PropTypes.string.isRequired, | ||
}; | ||
|
||
const defaultProps = { | ||
city: '', | ||
country: '', | ||
onAddressChanged: () => {}, | ||
shouldSaveDraft: false, | ||
state: '', | ||
street1: '', | ||
street2: '', | ||
submitButtonText: '', | ||
zip: '', | ||
}; | ||
|
||
function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldSaveDraft, state, street1, street2, submitButtonText, zip}) { | ||
const {translate} = useLocalize(); | ||
const zipSampleFormat = lodashGet(CONST.COUNTRY_ZIP_REGEX_DATA, [country, 'samples'], ''); | ||
const zipFormat = translate('common.zipCodeExampleFormat', {zipSampleFormat}); | ||
const isUSAForm = country === CONST.COUNTRY.US; | ||
|
||
/** | ||
* @param {Function} translate - translate function | ||
* @param {Boolean} isUSAForm - selected country ISO code is US | ||
* @param {Object} values - form input values | ||
* @returns {Object} - An object containing the errors for each inputID | ||
*/ | ||
const validator = useCallback((values) => { | ||
const errors = {}; | ||
const requiredFields = ['addressLine1', 'city', 'country', 'state']; | ||
|
||
// Check "State" dropdown is a valid state if selected Country is USA | ||
if (values.country === CONST.COUNTRY.US && !COMMON_CONST.STATES[values.state]) { | ||
errors.state = 'common.error.fieldRequired'; | ||
} | ||
|
||
// Add "Field required" errors if any required field is empty | ||
_.each(requiredFields, (fieldKey) => { | ||
if (ValidationUtils.isRequiredFulfilled(values[fieldKey])) { | ||
return; | ||
} | ||
errors[fieldKey] = 'common.error.fieldRequired'; | ||
}); | ||
|
||
// If no country is selected, default value is an empty string and there's no related regex data so we default to an empty object | ||
const countryRegexDetails = lodashGet(CONST.COUNTRY_ZIP_REGEX_DATA, values.country, {}); | ||
|
||
// The postal code system might not exist for a country, so no regex either for them. | ||
const countrySpecificZipRegex = lodashGet(countryRegexDetails, 'regex'); | ||
const countryZipFormat = lodashGet(countryRegexDetails, 'samples'); | ||
|
||
if (countrySpecificZipRegex) { | ||
if (!countrySpecificZipRegex.test(values.zipPostCode.trim().toUpperCase())) { | ||
if (ValidationUtils.isRequiredFulfilled(values.zipPostCode.trim())) { | ||
errors.zipPostCode = ['privatePersonalDetails.error.incorrectZipFormat', {zipFormat: countryZipFormat}]; | ||
} else { | ||
errors.zipPostCode = 'common.error.fieldRequired'; | ||
} | ||
} | ||
} else if (!CONST.GENERIC_ZIP_CODE_REGEX.test(values.zipPostCode.trim().toUpperCase())) { | ||
errors.zipPostCode = 'privatePersonalDetails.error.incorrectZipFormat'; | ||
} | ||
|
||
return errors; | ||
}, []); | ||
|
||
return ( | ||
<Form | ||
style={[styles.flexGrow1, styles.mh5]} | ||
formID={formID} | ||
validate={validator} | ||
onSubmit={onSubmit} | ||
submitButtonText={submitButtonText} | ||
enabledWhenOffline | ||
> | ||
<View style={styles.formSpaceVertical} /> | ||
<View> | ||
<AddressSearch | ||
inputID="addressLine1" | ||
label={translate('common.addressLine', {lineNumber: 1})} | ||
onValueChange={(data, key) => { | ||
onAddressChanged(data, key); | ||
// This enforces the country selector to use the country from address instead of the country from URL | ||
Navigation.setParams({country: undefined}); | ||
}} | ||
defaultValue={street1 || ''} | ||
renamedInputKeys={{ | ||
street: 'addressLine1', | ||
street2: 'addressLine2', | ||
city: 'city', | ||
state: 'state', | ||
zipCode: 'zipPostCode', | ||
country: 'country', | ||
}} | ||
maxInputLength={CONST.FORM_CHARACTER_LIMIT} | ||
shouldSaveDraft={shouldSaveDraft} | ||
/> | ||
</View> | ||
<View style={styles.formSpaceVertical} /> | ||
<TextInput | ||
inputID="addressLine2" | ||
label={translate('common.addressLine', {lineNumber: 2})} | ||
aria-label={translate('common.addressLine', {lineNumber: 2})} | ||
role={CONST.ACCESSIBILITY_ROLE.TEXT} | ||
defaultValue={street2 || ''} | ||
maxLength={CONST.FORM_CHARACTER_LIMIT} | ||
spellCheck={false} | ||
shouldSaveDraft={shouldSaveDraft} | ||
/> | ||
<View style={styles.formSpaceVertical} /> | ||
<View style={styles.mhn5}> | ||
<CountrySelector | ||
inputID="country" | ||
value={country} | ||
shouldSaveDraft={shouldSaveDraft} | ||
/> | ||
</View> | ||
<View style={styles.formSpaceVertical} /> | ||
{isUSAForm ? ( | ||
<View style={styles.mhn5}> | ||
<StatePicker | ||
inputID="state" | ||
defaultValue={state} | ||
onValueChange={onAddressChanged} | ||
shouldSaveDraft={shouldSaveDraft} | ||
/> | ||
</View> | ||
) : ( | ||
<TextInput | ||
inputID="state" | ||
label={translate('common.stateOrProvince')} | ||
aria-label={translate('common.stateOrProvince')} | ||
role={CONST.ACCESSIBILITY_ROLE.TEXT} | ||
value={state || ''} | ||
maxLength={CONST.FORM_CHARACTER_LIMIT} | ||
spellCheck={false} | ||
onValueChange={onAddressChanged} | ||
shouldSaveDraft={shouldSaveDraft} | ||
/> | ||
)} | ||
<View style={styles.formSpaceVertical} /> | ||
<TextInput | ||
inputID="city" | ||
label={translate('common.city')} | ||
aria-label={translate('common.city')} | ||
role={CONST.ACCESSIBILITY_ROLE.TEXT} | ||
defaultValue={city || ''} | ||
maxLength={CONST.FORM_CHARACTER_LIMIT} | ||
spellCheck={false} | ||
onValueChange={onAddressChanged} | ||
shouldSaveDraft={shouldSaveDraft} | ||
/> | ||
<View style={styles.formSpaceVertical} /> | ||
<TextInput | ||
inputID="zipPostCode" | ||
label={translate('common.zipPostCode')} | ||
aria-label={translate('common.zipPostCode')} | ||
role={CONST.ACCESSIBILITY_ROLE.TEXT} | ||
autoCapitalize="characters" | ||
defaultValue={zip || ''} | ||
maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE} | ||
hint={zipFormat} | ||
onValueChange={onAddressChanged} | ||
shouldSaveDraft={shouldSaveDraft} | ||
/> | ||
</Form> | ||
); | ||
} | ||
|
||
AddressForm.defaultProps = defaultProps; | ||
AddressForm.displayName = 'AddressForm'; | ||
AddressForm.propTypes = propTypes; | ||
|
||
export default AddressForm; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ import {withOnyx} from 'react-native-onyx'; | |
import _ from 'underscore'; | ||
import compose from '@libs/compose'; | ||
import * as ErrorUtils from '@libs/ErrorUtils'; | ||
import FormUtils from '@libs/FormUtils'; | ||
import * as ValidationUtils from '@libs/ValidationUtils'; | ||
import Visibility from '@libs/Visibility'; | ||
import stylePropTypes from '@styles/stylePropTypes'; | ||
|
@@ -303,7 +304,8 @@ function Form(props) { | |
|
||
// We want to initialize the input value if it's undefined | ||
if (_.isUndefined(inputValues[inputID])) { | ||
inputValues[inputID] = _.isBoolean(defaultValue) ? defaultValue : defaultValue || ''; | ||
// eslint-disable-next-line es/no-nullish-coalescing-operators | ||
inputValues[inputID] = defaultValue ?? ''; | ||
Comment on lines
+307
to
+308
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this being changed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I made this change because it's more simpler and does the same thing as before. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think that's correct. For example, previously, if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @pac-guerreiro @allroundexperts Shall we revert this line to avoid breaking other forms? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. although @allroundexperts is this the case you were referring to? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, actually, its the same. My bad. False alarm 🔕 |
||
} | ||
|
||
// We force the form to set the input value from the defaultValue props if there is a saved valid value | ||
|
@@ -553,7 +555,7 @@ export default compose( | |
key: (props) => props.formID, | ||
}, | ||
draftValues: { | ||
key: (props) => `${props.formID}Draft`, | ||
key: (props) => FormUtils.getDraftKey(props.formID), | ||
}, | ||
}), | ||
)(Form); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -250,7 +250,7 @@ function BaseTextInput(props) { | |
return ( | ||
<> | ||
<View | ||
style={styles.pointerEventsNone} | ||
style={[styles.pointerEventsNone, ...props.containerStyles]} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved |
||
// eslint-disable-next-line react/jsx-props-no-spreading | ||
{...(props.shouldInterceptSwipe && SwipeInterceptPanResponder.panHandlers)} | ||
> | ||
|
@@ -261,7 +261,6 @@ function BaseTextInput(props) { | |
style={[ | ||
props.autoGrowHeight && styles.autoGrowHeightInputContainer(textInputHeight, variables.componentSizeLarge, maxHeight), | ||
!isMultiline && styles.componentHeightLarge, | ||
...props.containerStyles, | ||
]} | ||
> | ||
<View | ||
|
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -873,6 +873,7 @@ export default { | |||||||
availableSpend: 'Remaining limit', | ||||||||
virtualCardNumber: 'Virtual card number', | ||||||||
physicalCardNumber: 'Physical card number', | ||||||||
getPhysicalCard: 'Get physical card', | ||||||||
reportFraud: 'Report virtual card fraud', | ||||||||
reviewTransaction: 'Review transaction', | ||||||||
suspiciousBannerTitle: 'Suspicious transaction', | ||||||||
|
@@ -903,6 +904,27 @@ export default { | |||||||
thatDidntMatch: "That didn't match the last 4 digits on your card. Please try again.", | ||||||||
}, | ||||||||
}, | ||||||||
getPhysicalCard: { | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can I get this translated to Spanish, please? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. bump @grgia There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We might have some of these already and could move them to Lines 1044 to 1046 in 9e35d91
|
||||||||
header: 'Get physical card', | ||||||||
nameMessage: 'Enter your first and last name, as this will be shown on your card.', | ||||||||
legalName: 'Legal name', | ||||||||
legalFirstName: 'Legal first name', | ||||||||
legalLastName: 'Legal last name', | ||||||||
phoneMessage: 'Enter your phone number.', | ||||||||
phoneNumber: 'Phone number', | ||||||||
address: 'Address', | ||||||||
addressMessage: 'Enter your shipping address.', | ||||||||
streetAddress: 'Street Address', | ||||||||
city: 'City', | ||||||||
state: 'State', | ||||||||
zipPostcode: 'Zip/Postcode', | ||||||||
country: 'Country', | ||||||||
confirmMessage: 'Please confirm your details below.', | ||||||||
estimatedDeliveryMessage: 'Your physical card will arrive in 2-3 business days.', | ||||||||
next: 'Next', | ||||||||
getPhysicalCard: 'Get physical card', | ||||||||
shipCard: 'Ship card', | ||||||||
}, | ||||||||
transferAmountPage: { | ||||||||
transfer: ({amount}: TransferParams) => `Transfer${amount ? ` ${amount}` : ''}`, | ||||||||
instant: 'Instant (Debit card)', | ||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -868,6 +868,7 @@ export default { | |
availableSpend: 'Límite restante', | ||
virtualCardNumber: 'Número de la tarjeta virtual', | ||
physicalCardNumber: 'Número de la tarjeta física', | ||
getPhysicalCard: 'Obtener tarjeta física', | ||
reportFraud: 'Reportar fraude con la tarjeta virtual', | ||
reviewTransaction: 'Revisar transacción', | ||
suspiciousBannerTitle: 'Transacción sospechosa', | ||
|
@@ -899,6 +900,28 @@ export default { | |
thatDidntMatch: 'Los 4 últimos dígitos de tu tarjeta no coinciden. Por favor, inténtalo de nuevo.', | ||
}, | ||
}, | ||
// TODO: add translation | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @joekaufmanexpensify I am still missing some spanish translations that I could not find in the codebase. Could you get them for me, please? 😄 |
||
getPhysicalCard: { | ||
header: 'Obtener tarjeta física', | ||
nameMessage: 'Introduce tu nombre y apellido como aparecerá en tu tarjeta.', | ||
legalName: 'Nombre completo', | ||
legalFirstName: 'Nombre legal', | ||
legalLastName: 'Apellidos legales', | ||
phoneMessage: 'Introduce tu número de teléfono.', | ||
phoneNumber: 'Número de teléfono', | ||
address: 'Dirección', | ||
addressMessage: 'Introduce tu dirección de envío.', | ||
streetAddress: 'Calle de dirección', | ||
city: 'Ciudad', | ||
state: 'Estado', | ||
zipPostcode: 'Código postal', | ||
country: 'País', | ||
confirmMessage: 'Por favor confirma tus datos.', | ||
estimatedDeliveryMessage: 'Tu tarjeta física llegará en 2-3 días laborales.', | ||
next: 'Siguiente', | ||
getPhysicalCard: 'Obtener tarjeta física', | ||
shipCard: 'Enviar tarjeta', | ||
}, | ||
transferAmountPage: { | ||
transfer: ({amount}: TransferParams) => `Transferir${amount ? ` ${amount}` : ''}`, | ||
instant: 'Instante', | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These please all need some comments above them like we have in other parts of the app.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pac-guerreiro could you address this comment