diff --git a/assets/images/product-illustrations/mushroom-top-hat.svg b/assets/images/product-illustrations/mushroom-top-hat.svg
new file mode 100644
index 000000000000..cb808f7289e0
--- /dev/null
+++ b/assets/images/product-illustrations/mushroom-top-hat.svg
@@ -0,0 +1,142 @@
+
+
+
diff --git a/package-lock.json b/package-lock.json
index 6090b2b1c0d2..aab783e8bbb7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -237,7 +237,7 @@
"style-loader": "^2.0.0",
"time-analytics-webpack-plugin": "^0.1.17",
"ts-node": "^10.9.2",
- "type-fest": "^3.12.0",
+ "type-fest": "^4.10.2",
"typescript": "^5.3.2",
"wait-port": "^0.2.9",
"webpack": "^5.76.0",
@@ -8139,9 +8139,9 @@
}
},
"node_modules/@pmmmwh/react-refresh-webpack-plugin": {
- "version": "0.5.10",
- "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz",
- "integrity": "sha512-j0Ya0hCFZPd4x40qLzbhGsh9TMtdb+CJQiso+WxLOPNasohq9cc5SNUcwsZaRH6++Xh91Xkm/xHCkuIiIu0LUA==",
+ "version": "0.5.11",
+ "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.11.tgz",
+ "integrity": "sha512-7j/6vdTym0+qZ6u4XbSAxrWBGYSdCfTzySkj7WAFgDLmSyWlOrWvpyzxlFh5jtw9dn0oL/jtW+06XfFiisN3JQ==",
"dev": true,
"dependencies": {
"ansi-html-community": "^0.0.8",
@@ -8161,7 +8161,7 @@
"@types/webpack": "4.x || 5.x",
"react-refresh": ">=0.10.0 <1.0.0",
"sockjs-client": "^1.4.0",
- "type-fest": ">=0.17.0 <4.0.0",
+ "type-fest": ">=0.17.0 <5.0.0",
"webpack": ">=4.43.0 <6.0.0",
"webpack-dev-server": "3.x || 4.x",
"webpack-hot-middleware": "2.x",
@@ -26870,10 +26870,11 @@
}
},
"node_modules/core-js-pure": {
- "version": "3.24.1",
+ "version": "3.36.0",
+ "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.36.0.tgz",
+ "integrity": "sha512-cN28qmhRNgbMZZMc/RFu5w8pK9VJzpb2rJVR/lHuZJKwmXnoWOpXmMkxqBB514igkp1Hu8WGROsiOAzUcKdHOQ==",
"dev": true,
"hasInstallScript": true,
- "license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
@@ -38622,6 +38623,17 @@
"node": ">=8"
}
},
+ "node_modules/jest-watch-typeahead/node_modules/type-fest": {
+ "version": "3.13.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz",
+ "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==",
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/jest-watcher": {
"version": "29.4.1",
"license": "MIT",
@@ -50849,11 +50861,12 @@
}
},
"node_modules/type-fest": {
- "version": "3.13.1",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz",
- "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==",
+ "version": "4.10.3",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.10.3.tgz",
+ "integrity": "sha512-JLXyjizi072smKGGcZiAJDCNweT8J+AuRxmPZ1aG7TERg4ijx9REl8CNhbr36RV4qXqL1gO1FF9HL8OkVmmrsA==",
+ "dev": true,
"engines": {
- "node": ">=14.16"
+ "node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
diff --git a/package.json b/package.json
index b99272e453b9..f5ff807cdbec 100644
--- a/package.json
+++ b/package.json
@@ -285,7 +285,7 @@
"style-loader": "^2.0.0",
"time-analytics-webpack-plugin": "^0.1.17",
"ts-node": "^10.9.2",
- "type-fest": "^3.12.0",
+ "type-fest": "^4.10.2",
"typescript": "^5.3.2",
"wait-port": "^0.2.9",
"webpack": "^5.76.0",
diff --git a/src/CONST.ts b/src/CONST.ts
index ce1295d2d71f..8abd4c087b16 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -3311,6 +3311,14 @@ const CONST = {
ADDRESS: 3,
},
},
+
+ EXIT_SURVEY: {
+ REASONS: {
+ FEATURE_NOT_AVAILABLE: 'featureNotAvailable',
+ DONT_UNDERSTAND: 'dontUnderstand',
+ PREFER_CLASSIC: 'preferClassic',
+ },
+ },
} as const;
type Country = keyof typeof CONST.ALL_COUNTRIES;
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index afbcd768b465..f0b400687b12 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -205,6 +205,9 @@ const ONYXKEYS = {
/** Is report data loading? */
IS_LOADING_APP: 'isLoadingApp',
+ /** Is the user in the process of switching to OldDot? */
+ IS_SWITCHING_TO_OLD_DOT: 'isSwitchingToOldDot',
+
/** Is the test tools modal open? */
IS_TEST_TOOLS_MODAL_OPEN: 'isTestToolsModalOpen',
@@ -388,6 +391,10 @@ const ONYXKEYS = {
REIMBURSEMENT_ACCOUNT_FORM_DRAFT: 'reimbursementAccountDraft',
PERSONAL_BANK_ACCOUNT: 'personalBankAccountForm',
PERSONAL_BANK_ACCOUNT_DRAFT: 'personalBankAccountFormDraft',
+ EXIT_SURVEY_REASON_FORM: 'exitSurveyReasonForm',
+ EXIT_SURVEY_REASON_FORM_DRAFT: 'exitSurveyReasonFormDraft',
+ EXIT_SURVEY_RESPONSE_FORM: 'exitSurveyResponseForm',
+ EXIT_SURVEY_RESPONSE_FORM_DRAFT: 'exitSurveyResponseFormDraft',
},
} as const;
@@ -410,6 +417,8 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.ROOM_SETTINGS_FORM]: FormTypes.RoomSettingsForm;
[ONYXKEYS.FORMS.NEW_TASK_FORM]: FormTypes.NewTaskForm;
[ONYXKEYS.FORMS.EDIT_TASK_FORM]: FormTypes.EditTaskForm;
+ [ONYXKEYS.FORMS.EXIT_SURVEY_REASON_FORM]: FormTypes.ExitSurveyReasonForm;
+ [ONYXKEYS.FORMS.EXIT_SURVEY_RESPONSE_FORM]: FormTypes.ExitSurveyResponseForm;
[ONYXKEYS.FORMS.MONEY_REQUEST_DESCRIPTION_FORM]: FormTypes.MoneyRequestDescriptionForm;
[ONYXKEYS.FORMS.MONEY_REQUEST_MERCHANT_FORM]: FormTypes.MoneyRequestMerchantForm;
[ONYXKEYS.FORMS.MONEY_REQUEST_AMOUNT_FORM]: FormTypes.MoneyRequestAmountForm;
@@ -534,6 +543,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.IS_LOADING_REPORT_DATA]: boolean;
[ONYXKEYS.IS_TEST_TOOLS_MODAL_OPEN]: boolean;
[ONYXKEYS.IS_LOADING_APP]: boolean;
+ [ONYXKEYS.IS_SWITCHING_TO_OLD_DOT]: boolean;
[ONYXKEYS.WALLET_TRANSFER]: OnyxTypes.WalletTransfer;
[ONYXKEYS.LAST_ACCESSED_WORKSPACE_POLICY_ID]: string;
[ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT]: boolean;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 11ffd06e0808..a8786bda3ffb 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -159,6 +159,17 @@ const ROUTES = {
getRoute: (source: string) => `settings/troubleshoot/console/share-log?source=${encodeURI(source)}` as const,
},
+ SETTINGS_EXIT_SURVEY_REASON: 'settings/exit-survey/reason',
+ SETTINGS_EXIT_SURVEY_RESPONSE: {
+ route: 'settings/exit-survey/response',
+ getRoute: (reason?: ValueOf, backTo?: string) =>
+ getUrlWithBackToParam(`settings/exit-survey/response${reason ? `?reason=${encodeURIComponent(reason)}` : ''}`, backTo),
+ },
+ SETTINGS_EXIT_SURVEY_CONFIRM: {
+ route: 'settings/exit-survey/confirm',
+ getRoute: (backTo?: string) => getUrlWithBackToParam('settings/exit-survey/confirm', backTo),
+ },
+
KEYBOARD_SHORTCUTS: 'keyboard-shortcuts',
NEW: 'new',
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index cdc22e9be69e..520895c89c98 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -80,6 +80,12 @@ const SCREENS = {
REPORT_VIRTUAL_CARD_FRAUD: 'Settings_Wallet_ReportVirtualCardFraud',
CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS: 'Settings_Wallet_Cards_Digital_Details_Update_Address',
},
+
+ EXIT_SURVEY: {
+ REASON: 'Settings_ExitSurvey_Reason',
+ RESPONSE: 'Settings_ExitSurvey_Response',
+ CONFIRM: 'Settings_ExitSurvey_Confirm',
+ },
},
SAVE_THE_WORLD: {
ROOT: 'SaveTheWorld_Root',
diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts
index ae98978ffcad..37d0f730c9e9 100644
--- a/src/components/Form/types.ts
+++ b/src/components/Form/types.ts
@@ -7,6 +7,7 @@ import type AmountTextInput from '@components/AmountTextInput';
import type CheckboxWithLabel from '@components/CheckboxWithLabel';
import type CountrySelector from '@components/CountrySelector';
import type Picker from '@components/Picker';
+import type RadioButtons from '@components/RadioButtons';
import type SingleChoiceQuestion from '@components/SingleChoiceQuestion';
import type StatePicker from '@components/StatePicker';
import type TextInput from '@components/TextInput';
@@ -34,7 +35,8 @@ type ValidInputs =
| typeof AmountForm
| typeof BusinessTypePicker
| typeof StatePicker
- | typeof ValuePicker;
+ | typeof ValuePicker
+ | typeof RadioButtons;
type ValueTypeKey = 'string' | 'boolean' | 'date';
type ValueTypeMap = {
diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts
index 9caa52bcc3bc..e03b393dc81f 100644
--- a/src/components/Icon/Illustrations.ts
+++ b/src/components/Icon/Illustrations.ts
@@ -16,6 +16,7 @@ import JewelBoxYellow from '@assets/images/product-illustrations/jewel-box--yell
import MagicCode from '@assets/images/product-illustrations/magic-code.svg';
import MoneyEnvelopeBlue from '@assets/images/product-illustrations/money-envelope--blue.svg';
import MoneyMousePink from '@assets/images/product-illustrations/money-mouse--pink.svg';
+import MushroomTopHat from '@assets/images/product-illustrations/mushroom-top-hat.svg';
import PaymentHands from '@assets/images/product-illustrations/payment-hands.svg';
import ReceiptYellow from '@assets/images/product-illustrations/receipt--yellow.svg';
import ReceiptsSearchYellow from '@assets/images/product-illustrations/receipts-search--yellow.svg';
@@ -96,6 +97,7 @@ export {
Mailbox,
MoneyEnvelopeBlue,
MoneyMousePink,
+ MushroomTopHat,
ReceiptsSearchYellow,
ReceiptYellow,
ReceiptWrangler,
diff --git a/src/components/RadioButtonWithLabel.tsx b/src/components/RadioButtonWithLabel.tsx
index 52464a1453a1..cfcd6acba41f 100644
--- a/src/components/RadioButtonWithLabel.tsx
+++ b/src/components/RadioButtonWithLabel.tsx
@@ -55,7 +55,7 @@ function RadioButtonWithLabel({LabelComponent, style, label = '', hasError = fal
accessible={false}
onPress={onPress}
style={[styles.flexRow, styles.flexWrap, styles.flexShrink1, styles.alignItemsCenter]}
- wrapperStyle={[styles.ml3, styles.pr2, styles.w100]}
+ wrapperStyle={[styles.flex1, styles.ml3, styles.pr2]}
// disable hover style when disabled
hoverDimmingValue={0.8}
pressDimmingValue={0.5}
diff --git a/src/components/RadioButtons.tsx b/src/components/RadioButtons.tsx
index 3407c5ad9afa..90c7d8580b5c 100644
--- a/src/components/RadioButtons.tsx
+++ b/src/components/RadioButtons.tsx
@@ -1,12 +1,16 @@
-import React, {useState} from 'react';
-import type {StyleProp, ViewStyle} from 'react-native';
+import React, {forwardRef, useEffect, useState} from 'react';
+import type {ForwardedRef} from 'react';
import {View} from 'react-native';
+import type {StyleProp, ViewStyle} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
+import type {MaybePhraseKey} from '@libs/Localize';
+import FormHelpMessage from './FormHelpMessage';
import RadioButtonWithLabel from './RadioButtonWithLabel';
type Choice = {
label: string;
value: string;
+ style?: StyleProp;
};
type RadioButtonsProps = {
@@ -19,33 +23,55 @@ type RadioButtonsProps = {
/** Callback to fire when selecting a radio button */
onPress: (value: string) => void;
+ /** Potential error text provided by a form InputWrapper */
+ errorText?: MaybePhraseKey;
+
/** Style for radio button */
radioButtonStyle?: StyleProp;
+
+ /** Callback executed when input value changes (same as onPress, but required by FormProvider for the sake of saving drafts) */
+ onInputChange?: (value: string) => void;
+
+ /** The checked value, if you're using this component as a controlled input. */
+ value?: string;
};
-function RadioButtons({items, onPress, defaultCheckedValue = '', radioButtonStyle}: RadioButtonsProps) {
+function RadioButtons({items, onPress, defaultCheckedValue = '', radioButtonStyle, errorText, onInputChange = () => {}, value}: RadioButtonsProps, ref: ForwardedRef) {
const styles = useThemeStyles();
const [checkedValue, setCheckedValue] = useState(defaultCheckedValue);
+ useEffect(() => {
+ if (value === checkedValue) {
+ return;
+ }
+ setCheckedValue(value ?? '');
+ }, [checkedValue, value]);
return (
-
- {items.map((item) => (
- {
- setCheckedValue(item.value);
- return onPress(item.value);
- }}
- label={item.label}
- />
- ))}
-
+ <>
+
+ {items.map((item) => (
+ {
+ setCheckedValue(item.value);
+ onInputChange(item.value);
+ return onPress(item.value);
+ }}
+ label={item.label}
+ />
+ ))}
+
+ {!!errorText && }
+ >
);
}
RadioButtons.displayName = 'RadioButtons';
export type {Choice};
-export default RadioButtons;
+export default forwardRef(RadioButtons);
diff --git a/src/components/SingleChoiceQuestion.tsx b/src/components/SingleChoiceQuestion.tsx
index c8bf783032ad..3ff844dd80e9 100644
--- a/src/components/SingleChoiceQuestion.tsx
+++ b/src/components/SingleChoiceQuestion.tsx
@@ -4,7 +4,6 @@ import React, {forwardRef} from 'react';
import type {Text as RNText} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
import type {MaybePhraseKey} from '@libs/Localize';
-import FormHelpMessage from './FormHelpMessage';
import type {Choice} from './RadioButtons';
import RadioButtons from './RadioButtons';
import Text from './Text';
@@ -32,8 +31,8 @@ function SingleChoiceQuestion({prompt, errorText, possibleAnswers, currentQuesti
items={possibleAnswers}
key={currentQuestionIndex}
onPress={onInputChange}
+ errorText={errorText}
/>
-
>
);
}
diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts
index ce0f0e126252..a0f3d62c3547 100644
--- a/src/components/TextInput/BaseTextInput/types.ts
+++ b/src/components/TextInput/BaseTextInput/types.ts
@@ -51,15 +51,11 @@ type CustomBaseTextInputProps = {
/**
* Autogrow input container length based on the entered text.
- * Note: If you use this prop, the text input has to be controlled
- * by a value prop.
*/
autoGrow?: boolean;
/**
* Autogrow input container height based on the entered text
- * Note: If you use this prop, the text input has to be controlled
- * by a value prop.
*/
autoGrowHeight?: boolean;
diff --git a/src/components/withKeyboardState.tsx b/src/components/withKeyboardState.tsx
index 74d10945fbcb..560576fdbf5c 100755
--- a/src/components/withKeyboardState.tsx
+++ b/src/components/withKeyboardState.tsx
@@ -8,27 +8,34 @@ import type ChildrenProps from '@src/types/utils/ChildrenProps';
type KeyboardStateContextValue = {
/** Whether the keyboard is open */
isKeyboardShown: boolean;
+
+ /** Height of the keyboard in pixels */
+ keyboardHeight: number;
};
// TODO: Remove - left for backwards compatibility with existing components (https://github.com/Expensify/App/issues/25151)
const keyboardStatePropTypes = {
/** Whether the keyboard is open */
isKeyboardShown: PropTypes.bool.isRequired,
+
+ /** Height of the keyboard in pixels */
+ keyboardHeight: PropTypes.number.isRequired,
};
const KeyboardStateContext = createContext({
isKeyboardShown: false,
+ keyboardHeight: 0,
});
function KeyboardStateProvider({children}: ChildrenProps): ReactElement | null {
- const [isKeyboardShown, setIsKeyboardShown] = useState(false);
+ const [keyboardHeight, setKeyboardHeight] = useState(0);
useEffect(() => {
- const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => {
- setIsKeyboardShown(true);
+ const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', (e) => {
+ setKeyboardHeight(e.endCoordinates.height);
});
const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => {
- setIsKeyboardShown(false);
+ setKeyboardHeight(0);
});
return () => {
@@ -39,9 +46,10 @@ function KeyboardStateProvider({children}: ChildrenProps): ReactElement | null {
const contextValue = useMemo(
() => ({
- isKeyboardShown,
+ keyboardHeight,
+ isKeyboardShown: keyboardHeight !== 0,
}),
- [isKeyboardShown],
+ [keyboardHeight],
);
return {children};
}
diff --git a/src/hooks/useNetwork.ts b/src/hooks/useNetwork.ts
index 1e4a6d4cf2ca..9d5e1e75d7c8 100644
--- a/src/hooks/useNetwork.ts
+++ b/src/hooks/useNetwork.ts
@@ -29,5 +29,5 @@ export default function useNetwork({onReconnect = () => {}}: UseNetworkProps = {
prevOfflineStatusRef.current = isOffline;
}, [isOffline]);
- return {isOffline};
+ return {isOffline: isOffline ?? false};
}
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 1626419985b6..4d7041d4a791 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -860,7 +860,6 @@ export default {
noLogsAvailable: 'No logs available',
logSizeTooLarge: ({size}: LogSizeParams) => `Log size exceeds the limit of ${size} MB. Please use "Save log" to download the log file instead.`,
},
- goToExpensifyClassic: 'Go to Expensify Classic',
security: 'Security',
signOut: 'Sign out',
signOutConfirmationText: "You'll lose any offline changes if you sign-out.",
@@ -2379,4 +2378,28 @@ export default {
mute: 'Mute',
unmute: 'Unmute',
},
+ exitSurvey: {
+ header: 'Before you go',
+ reasonPage: {
+ title: "Please tell us why you're leaving",
+ subtitle: 'Before you go, please tell us why you’d like to switch to Expensify Classic.',
+ },
+ reasons: {
+ [CONST.EXIT_SURVEY.REASONS.FEATURE_NOT_AVAILABLE]: "I need a feature that's only available in Expensify Classic.",
+ [CONST.EXIT_SURVEY.REASONS.DONT_UNDERSTAND]: "I don't understand how to use New Expensify.",
+ [CONST.EXIT_SURVEY.REASONS.PREFER_CLASSIC]: 'I understand how to use New Expensify, but I prefer Expensify Classic.',
+ },
+ prompts: {
+ [CONST.EXIT_SURVEY.REASONS.FEATURE_NOT_AVAILABLE]: "What feature do you need that isn't available in New Expensify?",
+ [CONST.EXIT_SURVEY.REASONS.DONT_UNDERSTAND]: 'What are you trying to do?',
+ [CONST.EXIT_SURVEY.REASONS.PREFER_CLASSIC]: 'Why do you prefer Expensify Classic?',
+ },
+ responsePlaceholder: 'Your response',
+ thankYou: 'Thanks for the feedback!',
+ thankYouSubtitle: 'Your responses will help us build a better product to get stuff done. Thank you so much!',
+ goToExpensifyClassic: 'Switch to Expensify Classic',
+ offlineTitle: "Looks like you're stuck here...",
+ offline:
+ "You appear to be offline. Unfortunately, Expensify Classic doesn't work offline, but New Expensify does. If you prefer to use Expensify Classic, try again when you have an internet connection.",
+ },
} satisfies TranslationBase;
diff --git a/src/languages/es.ts b/src/languages/es.ts
index d87d12e7f640..c9ff087d0de7 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -859,7 +859,6 @@ export default {
signOut: 'Desconectar',
signOutConfirmationText: 'Si cierras sesión perderás los cambios hechos mientras estabas desconectado',
versionLetter: 'v',
- goToExpensifyClassic: 'Ir a Expensify Classic',
readTheTermsAndPrivacy: {
phrase1: 'Leer los',
phrase2: 'Términos de Servicio',
@@ -2871,4 +2870,28 @@ export default {
mute: 'Silenciar',
unmute: 'Activar sonido',
},
+ exitSurvey: {
+ header: 'Antes de irte',
+ reasonPage: {
+ title: 'Dinos por qué te vas',
+ subtitle: 'Antes de irte, por favor dinos por qué te gustaría cambiarte a Expensify Classic.',
+ },
+ reasons: {
+ [CONST.EXIT_SURVEY.REASONS.FEATURE_NOT_AVAILABLE]: 'Necesito una función que sólo está disponible en Expensify Classic.',
+ [CONST.EXIT_SURVEY.REASONS.DONT_UNDERSTAND]: 'No entiendo cómo usar New Expensify.',
+ [CONST.EXIT_SURVEY.REASONS.PREFER_CLASSIC]: 'Entiendo cómo usar New Expensify, pero prefiero Expensify Classic.',
+ },
+ prompts: {
+ [CONST.EXIT_SURVEY.REASONS.FEATURE_NOT_AVAILABLE]: '¿Qué función necesitas que no esté disponible en New Expensify?',
+ [CONST.EXIT_SURVEY.REASONS.DONT_UNDERSTAND]: '¿Qué estás tratando de hacer?',
+ [CONST.EXIT_SURVEY.REASONS.PREFER_CLASSIC]: '¿Por qué prefieres Expensify Classic?',
+ },
+ responsePlaceholder: 'Su respuesta',
+ thankYou: '¡Gracias por tus comentarios!',
+ thankYouSubtitle: 'Sus respuestas nos ayudarán a crear un mejor producto para hacer las cosas bien. ¡Muchas gracias!',
+ goToExpensifyClassic: 'Cambiar a Expensify Classic',
+ offlineTitle: 'Parece que estás atrapado aquí...',
+ offline:
+ 'Parece que estás desconectado. Desafortunadamente, Expensify Classic no funciona sin conexión, pero New Expensify sí. Si prefieres utilizar Expensify Classic, inténtalo de nuevo cuando tengas conexión a internet.',
+ },
} satisfies EnglishTranslation;
diff --git a/src/libs/API/parameters/SwitchToOldDotParams.ts b/src/libs/API/parameters/SwitchToOldDotParams.ts
new file mode 100644
index 000000000000..95449a123dc9
--- /dev/null
+++ b/src/libs/API/parameters/SwitchToOldDotParams.ts
@@ -0,0 +1,9 @@
+import type {ValueOf} from 'type-fest';
+import type CONST from '@src/CONST';
+
+type SwitchToOldDotParams = {
+ reason?: ValueOf;
+ surveyResponse?: string;
+};
+
+export default SwitchToOldDotParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 66c6692b19fb..0b0a81eb21f8 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -147,3 +147,4 @@ export type {default as AcceptACHContractForBankAccount} from './AcceptACHContra
export type {default as UpdateWorkspaceDescriptionParams} from './UpdateWorkspaceDescriptionParams';
export type {default as SetWorkspaceAutoReportingParams} from './SetWorkspaceAutoReportingParams';
export type {default as SetWorkspaceApprovalModeParams} from './SetWorkspaceApprovalModeParams';
+export type {default as SwitchToOldDotParams} from './SwitchToOldDotParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index 571fab3404f1..17cc366ba3b7 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -149,6 +149,7 @@ const WRITE_COMMANDS = {
PAY_MONEY_REQUEST: 'PayMoneyRequest',
CANCEL_PAYMENT: 'CancelPayment',
ACCEPT_ACH_CONTRACT_FOR_BANK_ACCOUNT: 'AcceptACHContractForBankAccount',
+ SWITCH_TO_OLD_DOT: 'SwitchToOldDot',
} as const;
type WriteCommand = ValueOf;
@@ -296,6 +297,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.UPDATE_WORKSPACE_DESCRIPTION]: Parameters.UpdateWorkspaceDescriptionParams;
[WRITE_COMMANDS.SET_WORKSPACE_AUTO_REPORTING]: Parameters.SetWorkspaceAutoReportingParams;
[WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE]: Parameters.SetWorkspaceApprovalModeParams;
+ [WRITE_COMMANDS.SWITCH_TO_OLD_DOT]: Parameters.SwitchToOldDotParams;
};
const READ_COMMANDS = {
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
index cd75a6d31fdb..2be262aa5f0f 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
@@ -253,6 +253,9 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage').default as React.ComponentType,
[SCREENS.SETTINGS.REPORT_CARD_LOST_OR_DAMAGED]: () => require('../../../pages/settings/Wallet/ReportCardLostPage').default as React.ComponentType,
[SCREENS.KEYBOARD_SHORTCUTS]: () => require('../../../pages/KeyboardShortcutsPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.EXIT_SURVEY.REASON]: () => require('../../../pages/settings/ExitSurvey/ExitSurveyReasonPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.EXIT_SURVEY.RESPONSE]: () => require('../../../pages/settings/ExitSurvey/ExitSurveyResponsePage').default as React.ComponentType,
+ [SCREENS.SETTINGS.EXIT_SURVEY.CONFIRM]: () => require('../../../pages/settings/ExitSurvey/ExitSurveyConfirmPage').default as React.ComponentType,
});
const EnablePaymentsStackNavigator = createModalStackNavigator({
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index d98e19bb155e..48d649cc4dd9 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -273,6 +273,15 @@ const config: LinkingOptions['config'] = {
[SCREENS.SETTINGS.SHARE_CODE]: {
path: ROUTES.SETTINGS_SHARE_CODE,
},
+ [SCREENS.SETTINGS.EXIT_SURVEY.REASON]: {
+ path: ROUTES.SETTINGS_EXIT_SURVEY_REASON,
+ },
+ [SCREENS.SETTINGS.EXIT_SURVEY.RESPONSE]: {
+ path: ROUTES.SETTINGS_EXIT_SURVEY_RESPONSE.route,
+ },
+ [SCREENS.SETTINGS.EXIT_SURVEY.CONFIRM]: {
+ path: ROUTES.SETTINGS_EXIT_SURVEY_CONFIRM.route,
+ },
},
},
[SCREENS.RIGHT_MODAL.PRIVATE_NOTES]: {
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 2e00099b7966..f02bb3bd2aca 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -15,6 +15,7 @@ import type CONST from '@src/CONST';
import type NAVIGATORS from '@src/NAVIGATORS';
import type {HybridAppRoute, Route as Routes} from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
+import type EXIT_SURVEY_REASON_FORM_INPUT_IDS from '@src/types/form/ExitSurveyReasonForm';
type NavigationRef = NavigationContainerRefWithCurrent;
@@ -180,6 +181,14 @@ type SettingsNavigatorParamList = {
[SCREENS.SETTINGS.TWO_FACTOR_AUTH]: undefined;
[SCREENS.SETTINGS.REPORT_CARD_LOST_OR_DAMAGED]: undefined;
[SCREENS.KEYBOARD_SHORTCUTS]: undefined;
+ [SCREENS.SETTINGS.EXIT_SURVEY.REASON]: undefined;
+ [SCREENS.SETTINGS.EXIT_SURVEY.RESPONSE]: {
+ [EXIT_SURVEY_REASON_FORM_INPUT_IDS.REASON]: ValueOf;
+ backTo: Routes;
+ };
+ [SCREENS.SETTINGS.EXIT_SURVEY.CONFIRM]: {
+ backTo: Routes;
+ };
} & ReimbursementAccountNavigatorParamList;
type NewChatNavigatorParamList = {
diff --git a/src/libs/NumberUtils.ts b/src/libs/NumberUtils.ts
index d7eb87a2ed1e..60e5246f5ed2 100644
--- a/src/libs/NumberUtils.ts
+++ b/src/libs/NumberUtils.ts
@@ -69,4 +69,11 @@ function parseFloatAnyLocale(value: string): number {
return parseFloat(value ? value.replace(',', '.') : value);
}
-export {rand64, generateHexadecimalValue, generateRandomInt, parseFloatAnyLocale};
+/**
+ * Given an input number p and another number q, returns the largest number that's less than p and divisible by q.
+ */
+function roundDownToLargestMultiple(p: number, q: number) {
+ return Math.floor(p / q) * q;
+}
+
+export {rand64, generateHexadecimalValue, generateRandomInt, parseFloatAnyLocale, roundDownToLargestMultiple};
diff --git a/src/libs/actions/ExitSurvey.ts b/src/libs/actions/ExitSurvey.ts
new file mode 100644
index 000000000000..ef3ecd6d3e31
--- /dev/null
+++ b/src/libs/actions/ExitSurvey.ts
@@ -0,0 +1,78 @@
+import type {OnyxUpdate} from 'react-native-onyx';
+import Onyx from 'react-native-onyx';
+import * as API from '@libs/API';
+import ONYXKEYS from '@src/ONYXKEYS';
+import REASON_INPUT_IDS from '@src/types/form/ExitSurveyReasonForm';
+import type {ExitReason} from '@src/types/form/ExitSurveyReasonForm';
+import RESPONSE_INPUT_IDS from '@src/types/form/ExitSurveyResponseForm';
+
+let exitReason: ExitReason | undefined;
+let exitSurveyResponse: string | undefined;
+Onyx.connect({
+ key: ONYXKEYS.FORMS.EXIT_SURVEY_REASON_FORM,
+ callback: (value) => (exitReason = value?.[REASON_INPUT_IDS.REASON]),
+});
+Onyx.connect({
+ key: ONYXKEYS.FORMS.EXIT_SURVEY_RESPONSE_FORM,
+ callback: (value) => (exitSurveyResponse = value?.[RESPONSE_INPUT_IDS.RESPONSE]),
+});
+
+function saveExitReason(reason: ExitReason) {
+ Onyx.set(ONYXKEYS.FORMS.EXIT_SURVEY_REASON_FORM, {[REASON_INPUT_IDS.REASON]: reason});
+}
+
+function saveResponse(response: string) {
+ Onyx.set(ONYXKEYS.FORMS.EXIT_SURVEY_RESPONSE_FORM, {[RESPONSE_INPUT_IDS.RESPONSE]: response});
+}
+
+/**
+ * Save the user's response to the mandatory exit survey in the back-end.
+ */
+function switchToOldDot() {
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: ONYXKEYS.IS_SWITCHING_TO_OLD_DOT,
+ value: true,
+ },
+ ];
+
+ const finallyData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: ONYXKEYS.IS_SWITCHING_TO_OLD_DOT,
+ value: false,
+ },
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: ONYXKEYS.FORMS.EXIT_SURVEY_REASON_FORM,
+ value: null,
+ },
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: ONYXKEYS.FORMS.EXIT_SURVEY_REASON_FORM_DRAFT,
+ value: null,
+ },
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: ONYXKEYS.FORMS.EXIT_SURVEY_RESPONSE_FORM,
+ value: null,
+ },
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: ONYXKEYS.FORMS.EXIT_SURVEY_RESPONSE_FORM_DRAFT,
+ value: null,
+ },
+ ];
+
+ API.write(
+ 'SwitchToOldDot',
+ {
+ reason: exitReason,
+ surveyResponse: exitSurveyResponse,
+ },
+ {optimisticData, finallyData},
+ );
+}
+
+export {saveExitReason, saveResponse, switchToOldDot};
diff --git a/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx b/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx
new file mode 100644
index 000000000000..7459819afd99
--- /dev/null
+++ b/src/pages/settings/ExitSurvey/ExitSurveyConfirmPage.tsx
@@ -0,0 +1,107 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useCallback, useEffect} from 'react';
+import {View} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
+import Icon from '@components//Icon';
+import Button from '@components/Button';
+import FixedFooter from '@components/FixedFooter';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import {MushroomTopHat} from '@components/Icon/Illustrations';
+import ScreenWrapper from '@components/ScreenWrapper';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@navigation/Navigation';
+import type {SettingsNavigatorParamList} from '@navigation/types';
+import variables from '@styles/variables';
+import * as ExitSurvey from '@userActions/ExitSurvey';
+import * as Link from '@userActions/Link';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import type {ExitReason, ExitSurveyReasonForm} from '@src/types/form/ExitSurveyReasonForm';
+import EXIT_SURVEY_REASON_INPUT_IDS from '@src/types/form/ExitSurveyReasonForm';
+import ExitSurveyOffline from './ExitSurveyOffline';
+
+type ExitSurveyConfirmPageOnyxProps = {
+ exitReason?: ExitReason;
+ isLoading: OnyxEntry;
+};
+
+type ExitSurveyConfirmPageProps = ExitSurveyConfirmPageOnyxProps & StackScreenProps;
+
+function ExitSurveyConfirmPage({exitReason, isLoading, route, navigation}: ExitSurveyConfirmPageProps) {
+ const {translate} = useLocalize();
+ const {isOffline} = useNetwork();
+ const styles = useThemeStyles();
+
+ const getBackToParam = useCallback(() => {
+ if (isOffline) {
+ return ROUTES.SETTINGS;
+ }
+ if (exitReason) {
+ return ROUTES.SETTINGS_EXIT_SURVEY_RESPONSE.getRoute(exitReason, ROUTES.SETTINGS_EXIT_SURVEY_REASON);
+ }
+ return ROUTES.SETTINGS;
+ }, [exitReason, isOffline]);
+ const {backTo} = route.params;
+ useEffect(() => {
+ const newBackTo = getBackToParam();
+ if (backTo === newBackTo) {
+ return;
+ }
+ navigation.setParams({
+ backTo: newBackTo,
+ });
+ }, [backTo, getBackToParam, navigation]);
+
+ return (
+
+ Navigation.goBack(backTo)}
+ />
+
+ {isOffline && }
+ {!isOffline && (
+ <>
+
+ {translate('exitSurvey.thankYou')}
+ {translate('exitSurvey.thankYouSubtitle')}
+ >
+ )}
+
+
+
+
+ );
+}
+
+ExitSurveyConfirmPage.displayName = 'ExitSurveyConfirmPage';
+
+export default withOnyx({
+ exitReason: {
+ key: ONYXKEYS.FORMS.EXIT_SURVEY_REASON_FORM,
+ selector: (value: OnyxEntry) => value?.[EXIT_SURVEY_REASON_INPUT_IDS.REASON],
+ },
+ isLoading: {
+ key: ONYXKEYS.IS_SWITCHING_TO_OLD_DOT,
+ },
+})(ExitSurveyConfirmPage);
diff --git a/src/pages/settings/ExitSurvey/ExitSurveyOffline.tsx b/src/pages/settings/ExitSurvey/ExitSurveyOffline.tsx
new file mode 100644
index 000000000000..3363867ad4bb
--- /dev/null
+++ b/src/pages/settings/ExitSurvey/ExitSurveyOffline.tsx
@@ -0,0 +1,28 @@
+import React, {memo} from 'react';
+import {View} from 'react-native';
+import Icon from '@components/Icon';
+import {ToddBehindCloud} from '@components/Icon/Illustrations';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import variables from '@styles/variables';
+
+function ExitSurveyOffline() {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+ return (
+
+
+ {translate('exitSurvey.offlineTitle')}
+ {translate('exitSurvey.offline')}
+
+ );
+}
+
+ExitSurveyOffline.displayName = 'ExitSurveyOffline';
+
+export default memo(ExitSurveyOffline);
diff --git a/src/pages/settings/ExitSurvey/ExitSurveyReasonPage.tsx b/src/pages/settings/ExitSurvey/ExitSurveyReasonPage.tsx
new file mode 100644
index 000000000000..dbaf330803c1
--- /dev/null
+++ b/src/pages/settings/ExitSurvey/ExitSurveyReasonPage.tsx
@@ -0,0 +1,105 @@
+import React, {useEffect, useMemo, useState} from 'react';
+import {withOnyx} from 'react-native-onyx';
+import FormProvider from '@components/Form/FormProvider';
+import InputWrapper from '@components/Form/InputWrapper';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import type {Choice} from '@components/RadioButtons';
+import RadioButtons from '@components/RadioButtons';
+import ScreenWrapper from '@components/ScreenWrapper';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@navigation/Navigation';
+import * as ExitSurvey from '@userActions/ExitSurvey';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type {ExitReason} from '@src/types/form/ExitSurveyReasonForm';
+import INPUT_IDS from '@src/types/form/ExitSurveyReasonForm';
+import type {Errors} from '@src/types/onyx/OnyxCommon';
+import ExitSurveyOffline from './ExitSurveyOffline';
+
+type ExitSurveyReasonPageOnyxProps = {
+ draftReason: ExitReason | null;
+};
+
+function ExitSurveyReasonPage({draftReason}: ExitSurveyReasonPageOnyxProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+ const {isOffline} = useNetwork();
+
+ const [reason, setReason] = useState(draftReason);
+ useEffect(() => {
+ // disabling lint because || is fine to use as a logical operator (as opposed to being used to define a default value)
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ if (reason || !draftReason) {
+ return;
+ }
+ setReason(draftReason);
+ }, [reason, draftReason]);
+ const reasons: Choice[] = useMemo(
+ () =>
+ Object.values(CONST.EXIT_SURVEY.REASONS).map((value) => ({
+ value,
+ label: translate(`exitSurvey.reasons.${value}`),
+ style: styles.mt6,
+ })),
+ [styles, translate],
+ );
+
+ return (
+
+ Navigation.goBack()}
+ />
+ {
+ const errors: Errors = {};
+ if (!reason) {
+ errors[INPUT_IDS.REASON] = 'common.error.fieldRequired';
+ }
+ return errors;
+ }}
+ onSubmit={() => {
+ if (!reason) {
+ return;
+ }
+ ExitSurvey.saveExitReason(reason);
+ Navigation.navigate(ROUTES.SETTINGS_EXIT_SURVEY_RESPONSE.getRoute(reason, ROUTES.SETTINGS_EXIT_SURVEY_REASON));
+ }}
+ submitButtonText={translate('common.next')}
+ shouldValidateOnBlur
+ shouldValidateOnChange
+ >
+ {isOffline && }
+ {!isOffline && (
+ <>
+ {translate('exitSurvey.reasonPage.title')}
+ {translate('exitSurvey.reasonPage.subtitle')}
+ void}
+ shouldSaveDraft
+ />
+ >
+ )}
+
+
+ );
+}
+
+ExitSurveyReasonPage.displayName = 'ExitSurveyReasonPage';
+
+export default withOnyx({
+ draftReason: {
+ key: ONYXKEYS.FORMS.EXIT_SURVEY_REASON_FORM_DRAFT,
+ selector: (value) => value?.[INPUT_IDS.REASON] ?? null,
+ },
+})(ExitSurveyReasonPage);
diff --git a/src/pages/settings/ExitSurvey/ExitSurveyResponsePage.tsx b/src/pages/settings/ExitSurvey/ExitSurveyResponsePage.tsx
new file mode 100644
index 000000000000..c43ef8dd9320
--- /dev/null
+++ b/src/pages/settings/ExitSurvey/ExitSurveyResponsePage.tsx
@@ -0,0 +1,153 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useCallback, useEffect} from 'react';
+import {withOnyx} from 'react-native-onyx';
+import FormProvider from '@components/Form/FormProvider';
+import InputWrapper from '@components/Form/InputWrapper';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import type {AnimatedTextInputRef} from '@components/RNTextInput';
+import ScreenWrapper from '@components/ScreenWrapper';
+import Text from '@components/Text';
+import TextInput from '@components/TextInput';
+import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
+import useKeyboardState from '@hooks/useKeyboardState';
+import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
+import useSafeAreaInsets from '@hooks/useSafeAreaInsets';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useThemeStyles from '@hooks/useThemeStyles';
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import * as NumberUtils from '@libs/NumberUtils';
+import updateMultilineInputRange from '@libs/updateMultilineInputRange';
+import Navigation from '@navigation/Navigation';
+import type {SettingsNavigatorParamList} from '@navigation/types';
+import variables from '@styles/variables';
+import * as ExitSurvey from '@userActions/ExitSurvey';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import INPUT_IDS from '@src/types/form/ExitSurveyResponseForm';
+import type {Errors} from '@src/types/onyx/OnyxCommon';
+import ExitSurveyOffline from './ExitSurveyOffline';
+
+type ExitSurveyResponsePageOnyxProps = {
+ draftResponse: string;
+};
+
+type ExitSurveyResponsePageProps = ExitSurveyResponsePageOnyxProps & StackScreenProps;
+
+function ExitSurveyResponsePage({draftResponse, route, navigation}: ExitSurveyResponsePageProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+ const StyleUtils = useStyleUtils();
+ const {keyboardHeight} = useKeyboardState();
+ const {windowHeight} = useWindowDimensions();
+ const {top: safeAreaInsetsTop} = useSafeAreaInsets();
+
+ const {reason, backTo} = route.params;
+ const {isOffline} = useNetwork({
+ onReconnect: () => {
+ navigation.setParams({
+ backTo: ROUTES.SETTINGS_EXIT_SURVEY_REASON,
+ });
+ },
+ });
+ useEffect(() => {
+ if (!isOffline || backTo === ROUTES.SETTINGS) {
+ return;
+ }
+ navigation.setParams({backTo: ROUTES.SETTINGS});
+ }, [backTo, isOffline, navigation]);
+
+ const submitForm = useCallback(() => {
+ ExitSurvey.saveResponse(draftResponse);
+ Navigation.navigate(ROUTES.SETTINGS_EXIT_SURVEY_CONFIRM.getRoute(ROUTES.SETTINGS_EXIT_SURVEY_RESPONSE.route));
+ }, [draftResponse]);
+ useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER, submitForm);
+
+ const formTopMarginsStyle = styles.mt3;
+ const textStyle = styles.headerAnonymousFooter;
+ const baseResponseInputContainerStyle = styles.mt7;
+ const formMaxHeight = Math.floor(
+ windowHeight -
+ keyboardHeight -
+ safeAreaInsetsTop -
+ // Minus the height of HeaderWithBackButton
+ variables.contentHeaderHeight -
+ // Minus the top margins on the form
+ formTopMarginsStyle.marginTop,
+ );
+ const responseInputMaxHeight = NumberUtils.roundDownToLargestMultiple(
+ formMaxHeight -
+ // Minus the height of the text component
+ textStyle.lineHeight -
+ // Minus the response input margins (multiplied by 2 to create the effect of margins on top and bottom).
+ // marginBottom does not work in this case because the TextInput is in a ScrollView and will push the button beneath it out of view,
+ // so it's maxHeight is what dictates space between it and the button.
+ baseResponseInputContainerStyle.marginTop * 2 -
+ // Minus the approximate size of a default button
+ variables.componentSizeLarge -
+ // Minus the vertical margins around the form button
+ 40,
+
+ // Round down to the largest number of full lines
+ styles.baseTextInput.lineHeight,
+ );
+
+ return (
+
+ Navigation.goBack(backTo)}
+ />
+ {
+ const errors: Errors = {};
+ if (!draftResponse?.trim()) {
+ errors[INPUT_IDS.RESPONSE] = 'common.error.fieldRequired';
+ }
+ return errors;
+ }}
+ shouldValidateOnBlur
+ shouldValidateOnChange
+ >
+ {isOffline && }
+ {!isOffline && (
+ <>
+ {translate(`exitSurvey.prompts.${reason}`)}
+ {
+ if (!el) {
+ return;
+ }
+ updateMultilineInputRange(el);
+ }}
+ containerStyles={[baseResponseInputContainerStyle, StyleUtils.getMaximumHeight(responseInputMaxHeight)]}
+ shouldSaveDraft
+ />
+ >
+ )}
+
+
+ );
+}
+
+ExitSurveyResponsePage.displayName = 'ExitSurveyResponsePage';
+
+export default withOnyx({
+ draftResponse: {
+ key: ONYXKEYS.FORMS.EXIT_SURVEY_RESPONSE_FORM_DRAFT,
+ selector: (value) => value?.[INPUT_IDS.RESPONSE] ?? '',
+ },
+})(ExitSurveyResponsePage);
diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js
index fa031fb31697..f19df710b41a 100755
--- a/src/pages/settings/InitialSettingsPage.js
+++ b/src/pages/settings/InitialSettingsPage.js
@@ -141,6 +141,11 @@ function InitialSettingsPage(props) {
sectionStyle: styles.accountSettingsSectionContainer,
sectionTranslationKey: 'initialSettingsPage.account',
items: [
+ {
+ translationKey: 'exitSurvey.goToExpensifyClassic',
+ icon: Expensicons.ExpensifyLogoNew,
+ routeName: ROUTES.SETTINGS_EXIT_SURVEY_REASON,
+ },
{
translationKey: 'common.profile',
icon: Expensicons.Profile,
@@ -166,16 +171,6 @@ function InitialSettingsPage(props) {
icon: Expensicons.Lock,
routeName: ROUTES.SETTINGS_SECURITY,
},
- {
- translationKey: 'initialSettingsPage.goToExpensifyClassic',
- icon: Expensicons.ExpensifyLogoNew,
- action: () => {
- Link.openOldDotLink(CONST.OLDDOT_URLS.INBOX);
- },
- link: () => Link.buildOldDotURL(CONST.OLDDOT_URLS.INBOX),
- iconRight: Expensicons.NewWindow,
- shouldShowRightIcon: true,
- },
{
translationKey: 'initialSettingsPage.signOut',
icon: Expensicons.Exit,
@@ -211,7 +206,7 @@ function InitialSettingsPage(props) {
* Retuns a list of menu items data for general section
* @returns {Object} object with translationKey, style and items for the general section
*/
- const generaltMenuItemsData = useMemo(
+ const generalMenuItemsData = useMemo(
() => ({
sectionStyle: {
...styles.pt4,
@@ -320,7 +315,7 @@ function InitialSettingsPage(props) {
);
const accountMenuItems = useMemo(() => getMenuItemsSection(accountMenuItemsData), [accountMenuItemsData, getMenuItemsSection]);
- const generalMenuItems = useMemo(() => getMenuItemsSection(generaltMenuItemsData), [generaltMenuItemsData, getMenuItemsSection]);
+ const generalMenuItems = useMemo(() => getMenuItemsSection(generalMenuItemsData), [generalMenuItemsData, getMenuItemsSection]);
const currentUserDetails = props.currentUserPersonalDetails || {};
const avatarURL = lodashGet(currentUserDetails, 'avatar', '');
diff --git a/src/styles/variables.ts b/src/styles/variables.ts
index c6c29fdc4b79..f7c9bd055041 100644
--- a/src/styles/variables.ts
+++ b/src/styles/variables.ts
@@ -217,4 +217,7 @@ export default {
updateTextViewContainerWidth: 310,
updateViewHeaderHeight: 70,
workspaceProfileName: 20,
+
+ mushroomTopHatWidth: 138,
+ mushroomTopHatHeight: 128,
} as const;
diff --git a/src/types/form/ExitSurveyReasonForm.ts b/src/types/form/ExitSurveyReasonForm.ts
new file mode 100644
index 000000000000..48eddb026010
--- /dev/null
+++ b/src/types/form/ExitSurveyReasonForm.ts
@@ -0,0 +1,19 @@
+import type {ValueOf} from 'type-fest';
+import type CONST from '@src/CONST';
+import type Form from './Form';
+
+type ExitReason = ValueOf;
+
+const INPUT_IDS = {
+ REASON: 'reason',
+} as const;
+
+type ExitSurveyReasonForm = Form<
+ ValueOf,
+ {
+ [INPUT_IDS.REASON]: ExitReason;
+ }
+>;
+
+export type {ExitSurveyReasonForm, ExitReason};
+export default INPUT_IDS;
diff --git a/src/types/form/ExitSurveyResponseForm.ts b/src/types/form/ExitSurveyResponseForm.ts
new file mode 100644
index 000000000000..6e3458bd8e38
--- /dev/null
+++ b/src/types/form/ExitSurveyResponseForm.ts
@@ -0,0 +1,16 @@
+import type {ValueOf} from 'type-fest';
+import type Form from './Form';
+
+const INPUT_IDS = {
+ RESPONSE: 'response',
+} as const;
+
+type ExitSurveyResponseForm = Form<
+ ValueOf,
+ {
+ [INPUT_IDS.RESPONSE]: string;
+ }
+>;
+
+export type {ExitSurveyResponseForm};
+export default INPUT_IDS;
diff --git a/src/types/form/index.ts b/src/types/form/index.ts
index aa21aeb274fc..1ff8d0df2031 100644
--- a/src/types/form/index.ts
+++ b/src/types/form/index.ts
@@ -3,6 +3,8 @@ export type {CloseAccountForm} from './CloseAccountForm';
export type {DateOfBirthForm} from './DateOfBirthForm';
export type {DisplayNameForm} from './DisplayNameForm';
export type {EditTaskForm} from './EditTaskForm';
+export type {ExitSurveyReasonForm} from './ExitSurveyReasonForm';
+export type {ExitSurveyResponseForm} from './ExitSurveyResponseForm';
export type {GetPhysicalCardForm} from './GetPhysicalCardForm';
export type {HomeAddressForm} from './HomeAddressForm';
export type {IKnowTeacherForm} from './IKnowTeacherForm';