Skip to content

Commit

Permalink
Merge pull request Expensify#45481 from callstack-internal/pac-guerre…
Browse files Browse the repository at this point in the history
…iro/feature/debug-mode

Debug Mode - Report and Report Actions
  • Loading branch information
chiragsalian authored Sep 18, 2024
2 parents b7c0055 + 014b3d6 commit 8d19726
Show file tree
Hide file tree
Showing 43 changed files with 3,193 additions and 121 deletions.
9 changes: 9 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2804,6 +2804,7 @@ const CONST = {
MARK_AS_INCOMPLETE: 'markAsIncomplete',
CANCEL_PAYMENT: 'cancelPayment',
UNAPPROVE: 'unapprove',
DEBUG: 'debug',
},
EDIT_REQUEST_FIELD: {
AMOUNT: 'amount',
Expand Down Expand Up @@ -4139,6 +4140,7 @@ const CONST = {
CARD_AUTHENTICATION_REQUIRED: 'authentication_required',
},
TAB: {
DEBUG_TAB_ID: 'DebugTab',
NEW_CHAT_TAB_ID: 'NewChatTab',
NEW_CHAT: 'chat',
NEW_ROOM: 'room',
Expand Down Expand Up @@ -5753,6 +5755,13 @@ const CONST = {
CATEGORIES_ARTICLE_LINK: 'https://help.expensify.com/articles/expensify-classic/workspaces/Create-categories#import-custom-categories',
TAGS_ARTICLE_LINK: 'https://help.expensify.com/articles/expensify-classic/workspaces/Create-tags#import-a-spreadsheet-1',
},

DEBUG: {
DETAILS: 'details',
JSON: 'json',
REPORT_ACTIONS: 'actions',
REPORT_ACTION_PREVIEW: 'preview',
},
} as const;

type Country = keyof typeof CONST.ALL_COUNTRIES;
Expand Down
9 changes: 9 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,12 @@ const ONYXKEYS = {
RULES_MAX_EXPENSE_AMOUNT_FORM_DRAFT: 'rulesMaxExpenseAmountFormDraft',
RULES_MAX_EXPENSE_AGE_FORM: 'rulesMaxExpenseAgeForm',
RULES_MAX_EXPENSE_AGE_FORM_DRAFT: 'rulesMaxExpenseAgeFormDraft',
DEBUG_REPORT_PAGE_FORM: 'debugReportPageForm',
DEBUG_REPORT_PAGE_FORM_DRAFT: 'debugReportPageFormDraft',
DEBUG_REPORT_ACTION_PAGE_FORM: 'debugReportActionPageForm',
DEBUG_REPORT_ACTION_PAGE_FORM_DRAFT: 'debugReportActionPageFormDraft',
DEBUG_DETAILS_FORM: 'debugDetailsForm',
DEBUG_DETAILS_FORM_DRAFT: 'debugDetailsFormDraft',
},
} as const;

Expand Down Expand Up @@ -786,6 +792,9 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.RULES_MAX_EXPENSE_AMOUNT_FORM]: FormTypes.RulesMaxExpenseAmountForm;
[ONYXKEYS.FORMS.RULES_MAX_EXPENSE_AGE_FORM]: FormTypes.RulesMaxExpenseAgeForm;
[ONYXKEYS.FORMS.SEARCH_SAVED_SEARCH_RENAME_FORM]: FormTypes.SearchSavedSearchRenameForm;
[ONYXKEYS.FORMS.DEBUG_REPORT_PAGE_FORM]: FormTypes.DebugReportForm;
[ONYXKEYS.FORMS.DEBUG_REPORT_ACTION_PAGE_FORM]: FormTypes.DebugReportActionForm;
[ONYXKEYS.FORMS.DEBUG_DETAILS_FORM]: FormTypes.DebugReportForm | FormTypes.DebugReportActionForm;
};

type OnyxFormDraftValuesMapping = {
Expand Down
44 changes: 44 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1483,6 +1483,50 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/accounting/sage-intacct/advanced/payment-account',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/advanced/payment-account` as const,
},
DEBUG_REPORT: {
route: 'debug/report/:reportID',
getRoute: (reportID: string) => `debug/report/${reportID}` as const,
},
DEBUG_REPORT_TAB_DETAILS: {
route: 'debug/report/:reportID/details',
getRoute: (reportID: string) => `debug/report/${reportID}/details` as const,
},
DEBUG_REPORT_TAB_JSON: {
route: 'debug/report/:reportID/json',
getRoute: (reportID: string) => `debug/report/${reportID}/json` as const,
},
DEBUG_REPORT_TAB_ACTIONS: {
route: 'debug/report/:reportID/actions',
getRoute: (reportID: string) => `debug/report/${reportID}/actions` as const,
},
DEBUG_REPORT_ACTION: {
route: 'debug/report/:reportID/actions/:reportActionID',
getRoute: (reportID: string, reportActionID: string) => `debug/report/${reportID}/actions/${reportActionID}` as const,
},
DEBUG_REPORT_ACTION_CREATE: {
route: 'debug/report/:reportID/actions/create',
getRoute: (reportID: string) => `debug/report/${reportID}/actions/create` as const,
},
DEBUG_REPORT_ACTION_TAB_DETAILS: {
route: 'debug/report/:reportID/actions/:reportActionID/details',
getRoute: (reportID: string, reportActionID: string) => `debug/report/${reportID}/actions/${reportActionID}/details` as const,
},
DEBUG_REPORT_ACTION_TAB_JSON: {
route: 'debug/report/:reportID/actions/:reportActionID/json',
getRoute: (reportID: string, reportActionID: string) => `debug/report/${reportID}/actions/${reportActionID}/json` as const,
},
DEBUG_REPORT_ACTION_TAB_PREVIEW: {
route: 'debug/report/:reportID/actions/:reportActionID/preview',
getRoute: (reportID: string, reportActionID: string) => `debug/report/${reportID}/actions/${reportActionID}/preview` as const,
},
DETAILS_CONSTANT_PICKER_PAGE: {
route: 'debug/details/constant/:fieldName',
getRoute: (fieldName: string, fieldValue?: string, backTo?: string) => getUrlWithBackToParam(`debug/details/constant/${fieldName}?fieldValue=${fieldValue}`, backTo),
},
DETAILS_DATE_TIME_PICKER_PAGE: {
route: 'debug/details/datetime/:fieldName',
getRoute: (fieldName: string, fieldValue?: string, backTo?: string) => getUrlWithBackToParam(`debug/details/datetime/${fieldName}?fieldValue=${fieldValue}`, backTo),
},
} as const;

/**
Expand Down
8 changes: 8 additions & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ const SCREENS = {
RESTRICTED_ACTION: 'RestrictedAction',
REPORT_EXPORT: 'Report_Export',
MISSING_PERSONAL_DETAILS: 'MissingPersonalDetails',
DEBUG: 'Debug',
},
ONBOARDING_MODAL: {
ONBOARDING: 'Onboarding',
Expand Down Expand Up @@ -551,6 +552,13 @@ const SCREENS = {
FEATURE_TRAINING_ROOT: 'FeatureTraining_Root',
RESTRICTED_ACTION_ROOT: 'RestrictedAction_Root',
MISSING_PERSONAL_DETAILS_ROOT: 'MissingPersonalDetails_Root',
DEBUG: {
REPORT: 'Debug_Report',
REPORT_ACTION: 'Debug_Report_Action',
REPORT_ACTION_CREATE: 'Debug_Report_Action_Create',
DETAILS_CONSTANT_PICKER_PAGE: 'Debug_Details_Constant_Picker_Page',
DETAILS_DATE_TIME_PICKER_PAGE: 'Debug_Details_Date_Time_Picker_Page',
},
} as const;

type Screen = DeepValueOf<typeof SCREENS>;
Expand Down
10 changes: 10 additions & 0 deletions src/components/AccountSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ function AccountSwitcher() {
const {canUseNewDotCopilot} = usePermissions();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const [account] = useOnyx(ONYXKEYS.ACCOUNT);
const [session] = useOnyx(ONYXKEYS.SESSION);
const [user] = useOnyx(ONYXKEYS.USER);
const buttonRef = useRef<HTMLDivElement>(null);

const [shouldShowDelegatorMenu, setShouldShowDelegatorMenu] = useState(false);
Expand Down Expand Up @@ -166,6 +168,14 @@ function AccountSwitcher() {
>
{Str.removeSMSDomain(currentUserPersonalDetails?.login ?? '')}
</Text>
{!!user?.isDebugModeEnabled && (
<Text
style={[styles.textLabelSupporting, styles.mt1, styles.w100]}
numberOfLines={1}
>
AccountID: {session?.accountID}
</Text>
)}
</View>
</View>
</PressableWithFeedback>
Expand Down
2 changes: 1 addition & 1 deletion src/components/DatePicker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ type DatePickerProps = {
maxDate?: Date;

/** A function that is passed by FormWrapper */
onInputChange?: (value: Date) => void;
onInputChange?: (value: string) => void;

/** A function that is passed by FormWrapper */
onTouched?: () => void;
Expand Down
68 changes: 37 additions & 31 deletions src/components/Form/FormProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ type FormProviderProps<TFormID extends OnyxFormKey = OnyxFormKey> = FormProvider

/** Whether button is disabled */
isSubmitDisabled?: boolean;

/** Whether HTML is allowed in form inputs */
allowHTML?: boolean;
};

function FormProvider(
Expand All @@ -92,6 +95,7 @@ function FormProvider(
draftValues,
onSubmit,
shouldTrimValues = true,
allowHTML = false,
...rest
}: FormProviderProps,
forwardedRef: ForwardedRef<FormRef>,
Expand All @@ -114,40 +118,42 @@ function FormProvider(

const validateErrors: GenericFormInputErrors = validate?.(trimmedStringValues) ?? {};

// Validate the input for html tags. It should supersede any other error
Object.entries(trimmedStringValues).forEach(([inputID, inputValue]) => {
// If the input value is empty OR is non-string, we don't need to validate it for HTML tags
if (!inputValue || typeof inputValue !== 'string') {
return;
}
const foundHtmlTagIndex = inputValue.search(CONST.VALIDATE_FOR_HTML_TAG_REGEX);
const leadingSpaceIndex = inputValue.search(CONST.VALIDATE_FOR_LEADINGSPACES_HTML_TAG_REGEX);

// Return early if there are no HTML characters
if (leadingSpaceIndex === -1 && foundHtmlTagIndex === -1) {
return;
}

const matchedHtmlTags = inputValue.match(CONST.VALIDATE_FOR_HTML_TAG_REGEX);
let isMatch = CONST.WHITELISTED_TAGS.some((regex) => regex.test(inputValue));
// Check for any matches that the original regex (foundHtmlTagIndex) matched
if (matchedHtmlTags) {
// Check if any matched inputs does not match in WHITELISTED_TAGS list and return early if needed.
for (const htmlTag of matchedHtmlTags) {
isMatch = CONST.WHITELISTED_TAGS.some((regex) => regex.test(htmlTag));
if (!isMatch) {
break;
if (!allowHTML) {
// Validate the input for html tags. It should supersede any other error
Object.entries(trimmedStringValues).forEach(([inputID, inputValue]) => {
// If the input value is empty OR is non-string, we don't need to validate it for HTML tags
if (!inputValue || typeof inputValue !== 'string') {
return;
}
const foundHtmlTagIndex = inputValue.search(CONST.VALIDATE_FOR_HTML_TAG_REGEX);
const leadingSpaceIndex = inputValue.search(CONST.VALIDATE_FOR_LEADINGSPACES_HTML_TAG_REGEX);

// Return early if there are no HTML characters
if (leadingSpaceIndex === -1 && foundHtmlTagIndex === -1) {
return;
}

const matchedHtmlTags = inputValue.match(CONST.VALIDATE_FOR_HTML_TAG_REGEX);
let isMatch = CONST.WHITELISTED_TAGS.some((regex) => regex.test(inputValue));
// Check for any matches that the original regex (foundHtmlTagIndex) matched
if (matchedHtmlTags) {
// Check if any matched inputs does not match in WHITELISTED_TAGS list and return early if needed.
for (const htmlTag of matchedHtmlTags) {
isMatch = CONST.WHITELISTED_TAGS.some((regex) => regex.test(htmlTag));
if (!isMatch) {
break;
}
}
}
}

if (isMatch && leadingSpaceIndex === -1) {
return;
}
if (isMatch && leadingSpaceIndex === -1) {
return;
}

// Add a validation error here because it is a string value that contains HTML characters
validateErrors[inputID] = translate('common.error.invalidCharacter');
});
// Add a validation error here because it is a string value that contains HTML characters
validateErrors[inputID] = translate('common.error.invalidCharacter');
});
}

if (typeof validateErrors !== 'object') {
throw new Error('Validate callback must return an empty object or an object with shape {inputID: error}');
Expand All @@ -161,7 +167,7 @@ function FormProvider(

return touchedInputErrors;
},
[shouldTrimValues, formID, validate, errors, translate],
[shouldTrimValues, formID, validate, errors, translate, allowHTML],
);

// When locales change from another session of the same account,
Expand Down
4 changes: 3 additions & 1 deletion src/components/Form/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type StateSelector from '@components/StateSelector';
import type TextInput from '@components/TextInput';
import type TextPicker from '@components/TextPicker';
import type ValuePicker from '@components/ValuePicker';
import type ConstantSelector from '@pages/Debug/ConstantSelector';
import type BusinessTypePicker from '@pages/ReimbursementAccount/BusinessInfo/substeps/TypeBusiness/BusinessTypePicker';
import type DimensionTypeSelector from '@pages/workspace/accounting/intacct/import/DimensionTypeSelector';
import type NetSuiteCustomFieldMappingPicker from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomFieldMappingPicker';
Expand Down Expand Up @@ -61,7 +62,8 @@ type ValidInputs =
| typeof NetSuiteCustomFieldMappingPicker
| typeof NetSuiteMenuWithTopDescriptionForm
| typeof CountryPicker
| typeof StatePicker;
| typeof StatePicker
| typeof ConstantSelector;

type ValueTypeKey = 'string' | 'boolean' | 'date' | 'country' | 'reportFields' | 'disabledListValues';
type ValueTypeMap = {
Expand Down
8 changes: 8 additions & 0 deletions src/components/TabSelector/TabSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ type IconAndTitle = {

function getIconAndTitle(route: string, translate: LocaleContextProps['translate']): IconAndTitle {
switch (route) {
case CONST.DEBUG.DETAILS:
return {icon: Expensicons.Info, title: translate('debug.details')};
case CONST.DEBUG.JSON:
return {icon: Expensicons.Eye, title: translate('debug.JSON')};
case CONST.DEBUG.REPORT_ACTIONS:
return {icon: Expensicons.Document, title: translate('debug.reportActions')};
case CONST.DEBUG.REPORT_ACTION_PREVIEW:
return {icon: Expensicons.Document, title: translate('debug.reportActionPreview')};
case CONST.TAB_REQUEST.MANUAL:
return {icon: Expensicons.Pencil, title: translate('tabSelector.manual')};
case CONST.TAB_REQUEST.SCAN:
Expand Down
39 changes: 23 additions & 16 deletions src/components/TestToolMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ApiUtils from '@libs/ApiUtils';
Expand All @@ -17,19 +17,23 @@ import TestCrash from './TestCrash';
import TestToolRow from './TestToolRow';
import Text from './Text';

type TestToolMenuOnyxProps = {
/** User object in Onyx */
user: OnyxEntry<UserOnyx>;
};

type TestToolMenuProps = TestToolMenuOnyxProps & {
type TestToolMenuProps = {
/** Network object in Onyx */
network: OnyxEntry<NetworkOnyx>;
};
const USER_DEFAULT: UserOnyx = {shouldUseStagingServer: undefined, isSubscribedToNewsletter: false, validated: false, isFromPublicDomain: false, isUsingExpensifyCard: false};
const USER_DEFAULT: UserOnyx = {
shouldUseStagingServer: undefined,
isSubscribedToNewsletter: false,
validated: false,
isFromPublicDomain: false,
isUsingExpensifyCard: false,
isDebugModeEnabled: false,
};

function TestToolMenu({user = USER_DEFAULT, network}: TestToolMenuProps) {
function TestToolMenu({network}: TestToolMenuProps) {
const [user = USER_DEFAULT] = useOnyx(ONYXKEYS.USER);
const shouldUseStagingServer = user?.shouldUseStagingServer ?? ApiUtils.isUsingStagingApi();
const isDebugModeEnabled = !!user?.isDebugModeEnabled;
const styles = useThemeStyles();
const {translate} = useLocalize();

Expand All @@ -41,6 +45,15 @@ function TestToolMenu({user = USER_DEFAULT, network}: TestToolMenuProps) {
>
{translate('initialSettingsPage.troubleshoot.testingPreferences')}
</Text>
{/* When toggled the app will be put into debug mode. */}
<TestToolRow title={translate('initialSettingsPage.troubleshoot.debugMode')}>
<Switch
accessibilityLabel={translate('initialSettingsPage.troubleshoot.debugMode')}
isOn={isDebugModeEnabled}
onToggle={() => User.setIsDebugModeEnabled(!isDebugModeEnabled)}
/>
</TestToolRow>

{/* Option to switch between staging and default api endpoints.
This enables QA, internal testers and external devs to take advantage of sandbox environments for 3rd party services like Plaid and Onfido.
This toggle is not rendered for internal devs as they make environment changes directly to the .env file. */}
Expand Down Expand Up @@ -97,10 +110,4 @@ function TestToolMenu({user = USER_DEFAULT, network}: TestToolMenuProps) {

TestToolMenu.displayName = 'TestToolMenu';

export default withNetwork()(
withOnyx<TestToolMenuProps, TestToolMenuOnyxProps>({
user: {
key: ONYXKEYS.USER,
},
})(TestToolMenu),
);
export default withNetwork()(TestToolMenu);
Loading

0 comments on commit 8d19726

Please sign in to comment.