Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5194d1f
feat: do not show invalid phone in search
Prince-Mendiratta Apr 18, 2023
e94447c
feat: update header message on search
Prince-Mendiratta Apr 18, 2023
ed1c76b
fix: use possible instead of valid for verification
Prince-Mendiratta Apr 18, 2023
c92ef41
feat: allow searching for formatted numbers
Prince-Mendiratta Apr 18, 2023
00491d1
feat: get rid of Str.isValidPhone & use awsm-phonenum
Prince-Mendiratta Apr 18, 2023
d968394
feat: refactoring
Prince-Mendiratta Apr 18, 2023
31e3556
fix: undo refactor for isValidUSPhone
Prince-Mendiratta Apr 18, 2023
672ca37
feat: refactor isValidUSPhone and OptionListUtils
Prince-Mendiratta Apr 19, 2023
fec87b0
feat: refactor addSMSDomainIfPhoneNumber
Prince-Mendiratta Apr 19, 2023
ec1337d
feat: remove unused onyx key
Prince-Mendiratta Apr 19, 2023
47338e5
feat: remove unused regexes
Prince-Mendiratta Apr 20, 2023
6cccc0e
feat: add country code to input contact details
Prince-Mendiratta Apr 20, 2023
fd0e3dc
feat: refactor to use e164 format
Prince-Mendiratta Apr 21, 2023
c5793c6
feat: contactMethodPage submit e164 values to api
Prince-Mendiratta Apr 21, 2023
6dac1af
feat: deprecate getPhoneNumberWithoutUSCountryCodeAndSpecialChars
Prince-Mendiratta Apr 22, 2023
ca7fdd6
feat: update isValidUSPhone to ues countryCode
Prince-Mendiratta Apr 22, 2023
a85d5fa
fix: remove consolelog
Prince-Mendiratta Apr 22, 2023
dda2b80
fix: use CONST for US
Prince-Mendiratta Apr 22, 2023
e818808
fix: merge main
Prince-Mendiratta Apr 24, 2023
0b8e72b
fix: resolve more conflicts
Prince-Mendiratta Apr 25, 2023
d3c2239
fix: resolve conflicts
Prince-Mendiratta Apr 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions src/CONST.js
Original file line number Diff line number Diff line change
Expand Up @@ -962,15 +962,10 @@ const CONST = {
},
REGEX: {
SPECIAL_CHARS_WITHOUT_NEWLINE: /((?!\n)[()-\s\t])/g,
US_PHONE: /^\+1\d{10}$/,
US_PHONE_WITH_OPTIONAL_COUNTRY_CODE: /^(\+1)?\d{10}$/,
DIGITS_AND_PLUS: /^\+?[0-9]*$/,
PHONE_E164_PLUS: /^\+?[1-9]\d{1,14}$/,
PHONE_WITH_SPECIAL_CHARS: /^\s*(?:\+?(\d{1,3}))?[-. (]*(\d{3})[-. )]*(\d{3})[-. ]*(\d{4})(?: *x(\d+))?\s*$/,
ALPHABETIC_CHARS: /[a-zA-Z]+/,
ALPHABETIC_CHARS_WITH_NUMBER: /^[a-zA-Z0-9 ]*$/,
POSITIVE_INTEGER: /^\d+$/,
NON_ALPHA_NUMERIC: /[^A-Za-z0-9+]/g,
PO_BOX: /\b[P|p]?(OST|ost)?\.?\s*[O|o|0]?(ffice|FFICE)?\.?\s*[B|b][O|o|0]?[X|x]?\.?\s+[#]?(\d+)\b/,
ANY_VALUE: /^.+$/,
ZIP_CODE: /[0-9]{5}(?:[- ][0-9]{4})?/,
Expand All @@ -992,7 +987,6 @@ const CONST = {
// Extract attachment's source from the data's html string
ATTACHMENT_DATA: /(data-expensify-source|data-name)="([^"]+)"/g,

NON_NUMERIC_WITH_PLUS: /[^0-9+]/g,
EMOJI_NAME: /:[\w+-]+:/g,
EMOJI_SUGGESTIONS: /:[a-zA-Z0-9_+-]{1,40}$/,
AFTER_FIRST_LINE_BREAK: /\n.*/g,
Expand Down
16 changes: 1 addition & 15 deletions src/libs/LoginUtils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import Str from 'expensify-common/lib/str';
import Onyx from 'react-native-onyx';
import CONST from '../CONST';
import ONYXKEYS from '../ONYXKEYS';
Expand All @@ -19,30 +18,17 @@ function getPhoneNumberWithoutSpecialChars(phone) {
return phone.replace(CONST.REGEX.SPECIAL_CHARS_WITHOUT_NEWLINE, '');
}

/**
* Remove +1 and special chars from the phone number
*
* @param {String} phone
* @return {String}
*/
function getPhoneNumberWithoutUSCountryCodeAndSpecialChars(phone) {
return getPhoneNumberWithoutSpecialChars(phone.replace(/^\+1/, ''));
}

/**
* Append user country code to the phone number
*
* @param {String} phone
* @return {String}
*/
function appendCountryCode(phone) {
return (Str.isValidPhone(phone) && !phone.includes('+'))
? `+${countryCodeByIP}${phone}`
: phone;
return phone.startsWith('+') ? phone : `+${countryCodeByIP}${phone}`;
}

export {
getPhoneNumberWithoutSpecialChars,
getPhoneNumberWithoutUSCountryCodeAndSpecialChars,
appendCountryCode,
};
41 changes: 16 additions & 25 deletions src/libs/OptionsListUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Onyx from 'react-native-onyx';
import lodashOrderBy from 'lodash/orderBy';
import lodashGet from 'lodash/get';
import Str from 'expensify-common/lib/str';
import {parsePhoneNumber} from 'awesome-phonenumber';
import ONYXKEYS from '../ONYXKEYS';
import CONST from '../CONST';
import * as ReportUtils from './ReportUtils';
Expand Down Expand Up @@ -32,12 +33,6 @@ Onyx.connect({
callback: val => loginList = _.isEmpty(val) ? {} : val,
});

let countryCodeByIP;
Onyx.connect({
key: ONYXKEYS.COUNTRY_CODE,
callback: val => countryCodeByIP = val || 1,
});

let preferredLocale;
Onyx.connect({
key: ONYXKEYS.NVP_PREFERRED_LOCALE,
Expand Down Expand Up @@ -134,9 +129,9 @@ function getPolicyExpenseReportOptions(report) {
* @return {String}
*/
function addSMSDomainIfPhoneNumber(login) {
Comment thread
Prince-Mendiratta marked this conversation as resolved.
if (Str.isValidPhone(login) && !Str.isValidEmail(login)) {
const smsLogin = login + CONST.SMS.DOMAIN;
return smsLogin.includes('+') ? smsLogin : `+${countryCodeByIP}${smsLogin}`;
const parsedPhoneNumber = parsePhoneNumber(login);
if (parsedPhoneNumber.possible && !Str.isValidEmail(login)) {
return parsedPhoneNumber.number.e164 + CONST.SMS.DOMAIN;
}
return login;
}
Expand Down Expand Up @@ -532,8 +527,8 @@ function getOptions(reports, personalDetails, {
let recentReportOptions = [];
let personalDetailsOptions = [];
const reportMapForLogins = {};
const isPhoneNumber = CONST.REGEX.PHONE_WITH_SPECIAL_CHARS.test(searchInputValue);
const searchValue = isPhoneNumber ? searchInputValue.replace(CONST.REGEX.NON_NUMERIC_WITH_PLUS, '') : searchInputValue;
const parsedPhoneNumber = parsePhoneNumber(LoginUtils.appendCountryCode(searchInputValue));
const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number.e164 : searchInputValue;

// Filter out all the reports that shouldn't be displayed
const filteredReports = _.filter(reports, report => ReportUtils.shouldReportBeInOptionList(
Expand Down Expand Up @@ -676,25 +671,21 @@ function getOptions(reports, personalDetails, {
const noOptions = (recentReportOptions.length + personalDetailsOptions.length) === 0;
const noOptionsMatchExactly = !_.find(personalDetailsOptions.concat(recentReportOptions), option => option.login === searchValue.toLowerCase());

// If the phone number doesn't have an international code then let's prefix it with the
// current user's international code based on their IP address.
const login = LoginUtils.appendCountryCode(searchValue);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm is this the offending PR?

@s77rt Reverting this PR fixes the issue. I believe this line is the root cause, with old code we were appending the country code after we validate the phone number. So if the country code is +1 and we search for valid number with @expensify.sms , for example 5005550006@expensify.sms the old code will return +15005550006@expensify.sms, however with this PR , specifically this line

const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number.e164 : searchInputValue;

parsedPhoneNumber will fail for @expensify.sms and will fallback to the searchInputValue which is 5005550006@expensify.sms.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah so the bug is searching for 5005550006@expensify.sms won't work? I think you are right this is the offending PR. We didn't really check for this case at all 5005550006@expensify.sms does not seem like a valid login.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah so the bug is searching for 5005550006@expensify.sms won't work?

The bug is that when you are logged in using a phone number +15005550006, when inviting a new user to your workspace , without appending the country code , the code will fail to exclude your login and given that 5005550006@expensify.sms is a valid email format, it will give you the possibility to invite that user , later when the backend verify that it will return an error . We fixed that by :

  1. Always removing phone number domain @expensify.sms before validating a phone.
  2. Excluding any email with @expensify.sms domain.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fedirjh Yeah I got it, previously we used to append the country code to every input, but now we only do it for valid numbers. +5005550006@expensify.sms is not considered as a valid number so we end up using 5005550006@expensify.sms. Thanks!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting case. I would also think that we don't need to support @expensify.sms in search, but I guess there's no downside that we do now. Thanks!


if (login && (noOptions || noOptionsMatchExactly)
&& !isCurrentUser({login})
&& _.every(selectedOptions, option => option.login !== login)
&& ((Str.isValidEmail(login) && !Str.isDomainEmail(login)) || Str.isValidPhone(login))
&& (!_.find(loginOptionsToExclude, loginOptionToExclude => loginOptionToExclude.login === addSMSDomainIfPhoneNumber(login).toLowerCase()))
&& (login !== CONST.EMAIL.CHRONOS || Permissions.canUseChronos(betas))
if (searchValue && (noOptions || noOptionsMatchExactly)
&& !isCurrentUser({login: searchValue})
&& _.every(selectedOptions, option => option.login !== searchValue)
&& ((Str.isValidEmail(searchValue) && !Str.isDomainEmail(searchValue)) || parsedPhoneNumber.possible)
&& (!_.find(loginOptionsToExclude, loginOptionToExclude => loginOptionToExclude.login === addSMSDomainIfPhoneNumber(searchValue).toLowerCase()))
&& (searchValue !== CONST.EMAIL.CHRONOS || Permissions.canUseChronos(betas))
) {
userToInvite = createOption([login], personalDetails, null, reportActions, {
userToInvite = createOption([searchValue], personalDetails, null, reportActions, {
showChatPreviewLine,
});

// If user doesn't exist, use a default avatar
userToInvite.icons = [{
source: ReportUtils.getAvatar('', login),
name: login,
source: ReportUtils.getAvatar('', searchValue),
name: searchValue,
type: CONST.ICON_TYPE_AVATAR,
}];
}
Expand Down Expand Up @@ -864,7 +855,7 @@ function getHeaderMessage(hasSelectableOptions, hasUserToInvite, searchValue, ma
return Localize.translate(preferredLocale, 'common.maxParticipantsReached', {count: CONST.REPORT.MAXIMUM_PARTICIPANTS});
}

const isValidPhone = Str.isValidPhone(LoginUtils.appendCountryCode(searchValue));
const isValidPhone = parsePhoneNumber(LoginUtils.appendCountryCode(searchValue)).possible;

const isValidEmail = Str.isValidEmail(searchValue);

Expand Down
10 changes: 5 additions & 5 deletions src/libs/ValidationUtils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import moment from 'moment';
import _ from 'underscore';
import {URL_REGEX_WITH_REQUIRED_PROTOCOL} from 'expensify-common/lib/Url';
import {parsePhoneNumber} from 'awesome-phonenumber';
import CONST from '../CONST';
import * as CardUtils from './CardUtils';
import * as LoginUtils from './LoginUtils';
Expand Down Expand Up @@ -297,12 +298,11 @@ function validateIdentity(identity) {
* @returns {Boolean}
*/
function isValidUSPhone(phoneNumber = '', isCountryCodeOptional) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be consist with the recent change, I think it would be better if we benefit from the second param of parsePhoneNumber.

function isValidUSPhone(phoneNumber = '', isCountryCodeOptional) {
    const phone = phoneNumber || '';
    const regionCode = isCountryCodeOptional ? CONST.COUNTRY.US : null;

    const parsedPhoneNumber = parsePhoneNumber(phone, {regionCode});
    return parsedPhoneNumber.possible && parsedPhoneNumber.regionCode === CONST.COUNTRY.US;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@s77rt Noticed an issue here. We only allow US phone numbers, right? So the number +917838209999 won't work. The error however says Please enter a valid phone number. Should we instead change this to Please enter a valid US phone number?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is the error originating from?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't a console error, it's the form error. It's on the Step 2 of the VBA Flow, input phone number.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems unrelated to this PR. Let this be handled somewhere else.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good. I'll raise a bug report in slack to see if we want to tackle this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only allow US phone numbers, right?

That is not technically true. There are some others that are accepted, however currently we are not releasing a public list of them for some internal reasons. We should be opening up to all countries again shortly 😄

// Remove non alphanumeric characters from the phone number
const sanitizedPhone = (phoneNumber || '').replace(CONST.REGEX.NON_ALPHA_NUMERIC, '');
const isUsPhone = isCountryCodeOptional
? CONST.REGEX.US_PHONE_WITH_OPTIONAL_COUNTRY_CODE.test(sanitizedPhone) : CONST.REGEX.US_PHONE.test(sanitizedPhone);
const phone = phoneNumber || '';
const regionCode = isCountryCodeOptional ? CONST.COUNTRY.US : null;

return CONST.REGEX.PHONE_E164_PLUS.test(sanitizedPhone) && isUsPhone;
const parsedPhoneNumber = parsePhoneNumber(phone, {regionCode});
return parsedPhoneNumber.possible && parsedPhoneNumber.regionCode === CONST.COUNTRY.US;
}

/**
Expand Down
6 changes: 4 additions & 2 deletions src/pages/DetailsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import _ from 'underscore';
import {withOnyx} from 'react-native-onyx';
import Str from 'expensify-common/lib/str';
import lodashGet from 'lodash/get';
import {parsePhoneNumber} from 'awesome-phonenumber';
import styles from '../styles/styles';
import Text from '../components/Text';
import ONYXKEYS from '../ONYXKEYS';
Expand Down Expand Up @@ -73,8 +74,9 @@ const defaultProps = {
*/
const getPhoneNumber = (details) => {
Comment thread
Prince-Mendiratta marked this conversation as resolved.
// If the user hasn't set a displayName, it is set to their phone number, so use that
if (Str.isValidPhone(details.displayName)) {
return details.displayName;
const parsedPhoneNumber = parsePhoneNumber(details.displayName);
if (parsedPhoneNumber.possible) {
return parsedPhoneNumber.number.e164;
}

// If the user has set a displayName, get the phone number from the SMS login
Expand Down
4 changes: 2 additions & 2 deletions src/pages/EnablePayments/AdditionalDetailsStep.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
import {View} from 'react-native';
import moment from 'moment/moment';
import {parsePhoneNumber} from 'awesome-phonenumber';
import IdologyQuestions from './IdologyQuestions';
import ScreenWrapper from '../../components/ScreenWrapper';
import HeaderWithCloseButton from '../../components/HeaderWithCloseButton';
Expand All @@ -18,7 +19,6 @@ import TextLink from '../../components/TextLink';
import TextInput from '../../components/TextInput';
import * as Wallet from '../../libs/actions/Wallet';
import * as ValidationUtils from '../../libs/ValidationUtils';
import * as LoginUtils from '../../libs/LoginUtils';
import * as ErrorUtils from '../../libs/ErrorUtils';
import AddressForm from '../ReimbursementAccount/AddressForm';
import DatePicker from '../../components/DatePicker';
Expand Down Expand Up @@ -173,7 +173,7 @@ class AdditionalDetailsStep extends React.Component {
*/
activateWallet(values) {
const personalDetails = {
phoneNumber: LoginUtils.getPhoneNumberWithoutUSCountryCodeAndSpecialChars(values[INPUT_IDS.PHONE_NUMBER]),
phoneNumber: parsePhoneNumber(values[INPUT_IDS.PHONE_NUMBER], {regionCode: CONST.COUNTRY.US}).number.significant,
Comment thread
stitesExpensify marked this conversation as resolved.
legalFirstName: values[INPUT_IDS.LEGAL_FIRST_NAME],
legalLastName: values[INPUT_IDS.LEGAL_LAST_NAME],
addressStreet: values[INPUT_IDS.ADDRESS.street],
Expand Down
4 changes: 2 additions & 2 deletions src/pages/ReimbursementAccount/CompanyStep.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {View} from 'react-native';
import Str from 'expensify-common/lib/str';
import {withOnyx} from 'react-native-onyx';
import PropTypes from 'prop-types';
import {parsePhoneNumber} from 'awesome-phonenumber';
import HeaderWithCloseButton from '../../components/HeaderWithCloseButton';
import CONST from '../../CONST';
import * as BankAccounts from '../../libs/actions/BankAccounts';
Expand All @@ -18,7 +19,6 @@ import TextLink from '../../components/TextLink';
import StatePicker from '../../components/StatePicker';
import withLocalize from '../../components/withLocalize';
import * as ValidationUtils from '../../libs/ValidationUtils';
import * as LoginUtils from '../../libs/LoginUtils';
import compose from '../../libs/compose';
import ONYXKEYS from '../../ONYXKEYS';
import Picker from '../../components/Picker';
Expand Down Expand Up @@ -148,7 +148,7 @@ class CompanyStep extends React.Component {
// Fields from Company step
...values,
companyTaxID: values.companyTaxID.replace(CONST.REGEX.NON_NUMERIC, ''),
companyPhone: LoginUtils.getPhoneNumberWithoutUSCountryCodeAndSpecialChars(values.companyPhone),
companyPhone: parsePhoneNumber(values.companyPhone, {regionCode: CONST.COUNTRY.US}).number.significant,
Comment thread
stitesExpensify marked this conversation as resolved.
};

BankAccounts.updateCompanyInformationForBankAccount(bankAccount);
Expand Down
5 changes: 3 additions & 2 deletions src/pages/settings/Profile/Contacts/NewContactMethodPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {withOnyx} from 'react-native-onyx';
import {compose} from 'underscore';
import lodashGet from 'lodash/get';
import Str from 'expensify-common/lib/str';
import {parsePhoneNumber} from 'awesome-phonenumber';
import Button from '../../../../components/Button';
import FixedFooter from '../../../../components/FixedFooter';
import HeaderWithCloseButton from '../../../../components/HeaderWithCloseButton';
Expand Down Expand Up @@ -68,10 +69,10 @@ function NewContactMethodPage(props) {
}, []);

const isFormValid = useMemo(() => {
const phoneLogin = LoginUtils.getPhoneNumberWithoutSpecialChars(login);
const phoneLogin = LoginUtils.appendCountryCode(LoginUtils.getPhoneNumberWithoutSpecialChars(login));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to fix the bug here as mentioned in this comment #17578 (comment).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@s77rt Right now, I've added the check and conversion the the submit method as well. Do we instead want to combine both these functions and use a new state value for disabling the button instead?

@s77rt s77rt Apr 21, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's enough since the bug is technically on staging too.


return (Permissions.canUsePasswordlessLogins(props.betas) || password)
&& (Str.isValidEmail(login) || Str.isValidPhone(phoneLogin));
&& (Str.isValidEmail(login) || parsePhoneNumber(phoneLogin).possible);
}, [login, password, props.betas]);

const submitForm = useCallback(() => {
Expand Down
9 changes: 5 additions & 4 deletions src/pages/signin/LoginForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {withOnyx} from 'react-native-onyx';
import PropTypes from 'prop-types';
import _ from 'underscore';
import Str from 'expensify-common/lib/str';
import {parsePhoneNumber} from 'awesome-phonenumber';
import styles from '../../styles/styles';
import Text from '../../components/Text';
import * as Session from '../../libs/actions/Session';
Expand Down Expand Up @@ -143,10 +144,10 @@ class LoginForm extends React.Component {
return;
}

const phoneLogin = LoginUtils.getPhoneNumberWithoutSpecialChars(login);
const isValidPhoneLogin = Str.isValidPhone(phoneLogin);
const phoneLogin = LoginUtils.appendCountryCode(LoginUtils.getPhoneNumberWithoutSpecialChars(login));
Comment thread
Prince-Mendiratta marked this conversation as resolved.
const parsedPhoneNumber = parsePhoneNumber(phoneLogin);

if (!Str.isValidEmail(login) && !isValidPhoneLogin) {
if (!Str.isValidEmail(login) && !parsedPhoneNumber.possible) {
if (ValidationUtils.isNumericWithSpecialChars(login)) {
this.setState({formError: 'common.error.phoneNumber'});
} else {
Expand All @@ -160,7 +161,7 @@ class LoginForm extends React.Component {
});

// Check if this login has an account associated with it or not
Session.beginSignIn(isValidPhoneLogin ? phoneLogin : login);
Session.beginSignIn(parsedPhoneNumber.possible ? parsedPhoneNumber.number.e164 : login);
}

render() {
Expand Down