Tracking Issue: https://github.com/Expensify/Expensify/issues/377671
Design Doc Section: https://docs.google.com/document/d/1WubNv_VAv78IxG4FKsi9aS0pWESfWvUYbqBAAy5b4bc/edit#heading=h.m7jq555xj2u
HOLD ON https://github.com/Expensify/Expensify/issues/403611
We'll add a new page to guide users through setting up NetSuite to work with Expensify and inputting their tokens to initiate the connection.
- Route:
/settings/workspaces/{policyID}/accounting/netsuite/token-input
- PageComponent:
NetSuiteTokenInputPage
- Calls ConnectPolicyToNetSuite
Parameters: JSON object consisting of the fields described above.
policyID
accountID: NetSuite account Id
tokenID: Access token Id
tokenSecret: Token secret
This screen takes care of authenticating the NetSuite account with Expensify. While the screen shows 5 steps, the first 4 steps are just informational instructions the user needs to perform on their NetSuite account. Hence this would be a pseudo-multi-step form, which would look similar to the Add bank account workflow but the form will only exist in the last step.

The reference for such pseudo-form steps would be similar to FeesStep.
function InstallExpensifyBundle({onNext, screenIndex}: SubStepProps) { // Component name based on the step
const styles = useThemeStyles();
const {translate} = useLocalize();
const titleKey = `workspace.netsuite.tokenInputSteps.${NET_SUITE_TOKEN_INPUT_FORM.STEP_NAMES[screenIndex]}.title` as TranslationPath; // The key for the title based on the step. For example, enableTokenBasedAuth.title. We put these step names in the constant and fetch by the current screenIndex. This ensures we only add 1 component for the first 4 steps.
const descriptionKey = `workspace.netsuite.tokenInputSteps.${NET_SUITE_TOKEN_INPUT_FORM.STEP_NAMES[screenIndex]}.description` as TranslationPath; // The key for the description based on the step. For example, enableTokenBasedAuth.description
return (
<ScrollView style={styles.flex1}>
<Text style={[styles.textHeadlineLineHeightXXL, styles.ph5, styles.mb3]}>{titleKey}</Text>
<View style={[styles.ph5]}>
<Text>{descriptionKey} </Text>
<Button
success
large
style={[styles.w100, styles.mv5]}
onPress={onNext}
text={translate('common.next')}
/>
</View>
</ScrollView>
);
}
The above screen is the form component step used to take the input from the user, which follows the structure as:
TokenInputFormStep
const STEP_FIELDS = [NET_SUITE_TOKEN_INPUT_KEY.ACCOUNT_ID, NET_SUITE_TOKEN_INPUT_KEY.TOKEN_ID, NET_SUITE_TOKEN_INPUT_KEY.TOKEN_SECRET]; // All the fields relevant for the authentication.
function TokenInputFormStep({onNext, isEditing}: SubStepProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const [netSuiteTokenInput] = useOnyx(ONYXKEYS.NET_SUITE_TOKEN_INPUT);
const defaultValues = {
accountId: netSuiteTokenInput?.[NET_SUITE_TOKEN_INPUT_KEY.ACCOUNT_ID] ?? '',
tokenId: netSuiteTokenInput?.[NET_SUITE_TOKEN_INPUT_KEY.TOKEN_ID] ?? '',
tokenSecret: netSuiteTokenInput?.[NET_SUITE_TOKEN_INPUT_KEY.TOKEN_SECRET] ?? '',
};
const handleSubmit = useNetSuiteTokenInputStepFormSubmit({
fieldIds: STEP_FIELDS,
onNext,
shouldSaveDraft: isEditing,
});
return (
<FormProvider
formID={ONYXKEYS.FORMS.NET_SUITE_TOKEN_INPUT}
submitButtonText={translate(isEditing ? 'common.confirm' : 'common.next')}
validate={validate}
onSubmit={handleSubmit}
style={[styles.mh5, styles.flexGrow1]}
>
<View>
<Text style={[styles.textHeadlineLineHeightXXL, styles.mb3]}>{translate('accounting.netsuite.tokenInput.form.credentials')}</Text>
<View style={[styles.flex1]}>
<InputWrapper
InputComponent={TextInput} // All the inputs are going to be TextInput.
inputID={NET_SUITE_TOKEN_INPUT_KEY.accountId}
label={translate('accounting.netsuite.tokenInput.form.accountId')} // Whether we should have a title or description segregation.
aria-label={translate('accounting.netsuite.tokenInput.form.accountId')}
role={CONST.ROLE.PRESENTATION}
defaultValue={defaultValues.accountId}
shouldSaveDraft={!isEditing}
containerStyles={[styles.mt6]}
/>
<Text>{translate('accounting.netsuite.tokenInput.form.accountIdDescription')}<Text> // Covers for the text 'In NetSuite goto Step > Integration > SOAP Web Services Preferences.'
</View>
<View style={[styles.flex1]}>
<InputWrapper
InputComponent={TextInput} // All the inputs are going to be TextInput.
inputID={NET_SUITE_TOKEN_INPUT_KEY.tokenId}
label={translate('accounting.netsuite.tokenInput.form.tokenId)}
aria-label={translate('accounting.netsuite.tokenInput.form.tokenId)}
role={CONST.ROLE.PRESENTATION}
defaultValue={defaultValues.tokenId}
shouldSaveDraft={!isEditing}
containerStyles={[styles.mt6]}
/>
</View>
<View style={[styles.flex1]}>
<InputWrapper
InputComponent={TextInput} // All the inputs are going to be TextInput.
inputID={NET_SUITE_TOKEN_INPUT_KEY.tokenSecret}
label={translate('accounting.netsuite.tokenInput.form.tokenSecret')}
aria-label={translate('accounting.netsuite.tokenInput.form.tokenSecret)} // Confirm if secret is going to be a secured field like password.
role={CONST.ROLE.PRESENTATION}
defaultValue={defaultValues.tokenSecret}
shouldSaveDraft={!isEditing}
containerStyles={[styles.mt6]}
/>
</View>
</View>
</FormProvider>
);
}
TokenInputFormStep.displayName = TokenInputFormStep;
export default TokenInputFormStep;
Form Container component
function NetSuiteTokenInputForm({ netSuiteTokenInputForm }: TokenInputProps) { // Serves as a container for all the substeps and the form.
const {translate} = useLocalize();
const styles = useThemeStyles();
const values = useMemo(() => getSubstepValues(NET_SUITE_TOKEN_INPUT_KEY, netSuiteTokenInputForm), [netSuiteTokenInputForm]); // Considering it's related to auth and we'll store sensitive data, we shouldn't store the draft here.
const submit = () => {
// API Call comes here
Navigation.goBack(ROUTES.POLICY_ACCOUNTNG.getRoute(policyID));
};
const startFrom = useMemo(() => getInitialValuesForTokenInput(values), [values]);
const {
componentToRender: SubStep,
isEditing,
nextScreen,
prevScreen,
moveTo,
screenIndex,
goToTheLastStep,
} = useSubStep({
bodyContent,
startFrom,
onFinished: submit,
});
return (
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
testID={NetSuiteTokenInputForm.displayName}
>
<HeaderWithBackButton
title={translate('accounting.netsuite.tokenInput.formTitle')}
onBackButtonPress={handleBackButtonPress}
/>
<View style={[styles.ph5, styles.mb5, styles.mt3,]}>
<InteractiveStepSubHeader // Steps wrapper base component for next button based navigation
startStepIndex={1}
stepNames={CONST.NET_SUITE_TOKEN_INPUT_FORM.STEP_NAMES}
/>
</View>
<SubStep
isEditing={isEditing}
onNext={nextScreen}
onMove={moveTo}
/>
</ScreenWrapper>
);
}
NetSuiteTokenInputForm.displayName = NetSuiteTokenInputForm;
export default withOnyx<TokenInputProps, TokenInputPropsOnyxProps>({
netSuiteTokenInputForm: {
key: ONYXKEYS.FORMS.NET_SUITE_TOKEN_INPUT_FORM,
},
})(NetSuiteTokenInputForm);
Issue Owner
Current Issue Owner: @
Issue Owner
Current Issue Owner: @miljakljajic
Tracking Issue: https://github.com/Expensify/Expensify/issues/377671
Design Doc Section: https://docs.google.com/document/d/1WubNv_VAv78IxG4FKsi9aS0pWESfWvUYbqBAAy5b4bc/edit#heading=h.m7jq555xj2u
HOLD ON https://github.com/Expensify/Expensify/issues/403611
We'll add a new page to guide users through setting up NetSuite to work with Expensify and inputting their tokens to initiate the connection.
/settings/workspaces/{policyID}/accounting/netsuite/token-inputNetSuiteTokenInputPageParameters: JSON object consisting of the fields described above.
policyIDaccountID: NetSuite account IdtokenID: Access token IdtokenSecret: Token secretThis screen takes care of authenticating the NetSuite account with Expensify. While the screen shows 5 steps, the first 4 steps are just informational instructions the user needs to perform on their NetSuite account. Hence this would be a pseudo-multi-step form, which would look similar to the Add bank account workflow but the form will only exist in the last step.
The reference for such pseudo-form steps would be similar to FeesStep.
The above screen is the form component step used to take the input from the user, which follows the structure as:
TokenInputFormStep
Form Container component
Issue Owner
Current Issue Owner: @Issue Owner
Current Issue Owner: @miljakljajic