Skip to content

[HOLD for payment 2024-07-22] [HOLD for payment 2024-07-17] [HOLD for payment 2024-07-10] [#Wave-Control: Add NetSuite] Add Support for NetSuite Token Input #43434

Description

@yuwenmemon

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.

AD_4nXdec5Q7PXSwzTFhOf-oKklLsXh2zq7wZaKTZrBWiWNuAD30NLX0H2MMjtDmM0MUamA8ndUapT187n0dHC0XThi9xIrW_gc7NPfPGKeKtsRxsgDt339SBIqK

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>
    );
}
AD_4nXfSWe70sdSHMJPZ-0e7FmNOXounhYNS-pllcX0pojdBgcZgPZD3iGxZWgZi09R1f8nRqrEEWiilLoT3_qfbbLW_oCqdgX3XPy98qKlNPJPn7pejxxM9nY4I

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 OwnerCurrent Issue Owner: @
Issue OwnerCurrent Issue Owner: @miljakljajic

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionExternalAdded to denote the issue can be worked on by a contributorNewFeatureSomething to build that is a new item.WeeklyKSv2

Type

No type
No fields configured for issues without a type.

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions