Skip to content

Latest commit

 

History

History
1557 lines (1347 loc) · 60.2 KB

File metadata and controls

1557 lines (1347 loc) · 60.2 KB

Dynamics 365 Commerce - online extensibility samples

License

License is listed in the LICENSE file.

Sample - Add Delivery Notes attribute

Overview

This sample will demonstrate how to add Delivery Notes as a cart attribute in shipping address section. When a customer places an order, in the shipping address customer can fill delivery notes which is updated in checkout cart to use this new attribute.

Overview

Starter kit license

License for starter kit is listed in the LICENSE .

Prerequisites

Follow the instructions mentioned in document to set up the development environment.

Procedure to create custom theme

Follow the instructions mentioned in document to create the custom theme

Create a theme folder with name fabrikam-extended.

Detailed Steps

1. Module ejection

We clone the "checkout-shipping-address" module into "custom-checkout-shipping-address" which contains the changes for the checkout flow. Please refer document to clone module.

2. Add delivery notes option resource key to the checkout shipping address.

Add below resource key under src/modules/custom-checkout-shipping-address/custom-checkout-shipping-address.definition.json.

     "deliveryNotesPlaceHolderText": {
            "comment": "Delivery notes place holder text",
            "value": "Delivery Notes"
        }

3. Add logic for attribute in the custom-checkout-shipping-address.tsx

Implement the logic as shown below in the sample code to add attribute into shipping address as delivery notes input box in src/modules/custom-checkout-shipping-address/custom-checkout-shipping-address.tsx.

/*--------------------------------------------------------------
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * See License.txt in the project root for license information.
 *--------------------------------------------------------------*/

/* eslint-disable no-duplicate-imports */
import {
    Address,
    AddressPurpose,
    CartLine,
    CountryRegionInfo,
    SimpleProduct,
    StateProvinceInfo,
    TenderLine,
    TokenizedPaymentCard
} from '@msdyn365-commerce/retail-proxy';
import { IModuleStateProps, withModuleState } from '@msdyn365-commerce-modules/checkout-utilities';
import { getFallbackImageUrl, getSimpleProducts, ObjectExtensions, ProductInput } from '@msdyn365-commerce-modules/retail-actions';
import { format, getTelemetryObject, IModuleProps, ITelemetryContent } from '@msdyn365-commerce-modules/utilities';
import classnames from 'classnames';
import { action, computed, observable, reaction, set } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';

import { AutoSuggest } from '@msdyn365-commerce-modules/address';
import { IAutoSuggestOptions } from '@msdyn365-commerce-modules/address';
import { AddressCommon } from '@msdyn365-commerce-modules/address';
import { AddressFormat } from '@msdyn365-commerce-modules/address';
import { AddressItemType } from '@msdyn365-commerce-modules/address';
import { AddressMetaData } from '@msdyn365-commerce-modules/address';
import { AddressOperation, AddressType, IAddressResource, IAddressResponse } from '@msdyn365-commerce-modules/address';
import { AddressAddUpdate, IAddressAddUpdateProps } from '@msdyn365-commerce-modules/address';
import { AddressSelect, IAddressSelectProps } from '@msdyn365-commerce-modules/address';
import { AddressShow, IAddressShowProps } from '@msdyn365-commerce-modules/address';
import { ICustomCheckoutShippingAddressData } from './custom-checkout-shipping-address.data';
import { ICustomCheckoutShippingAddressProps } from './custom-checkout-shipping-address.props.autogenerated';
import {
    CheckoutPickupCartLines,
    defaultImageSettings,
    ICartLineWithProduct,
    ICheckoutShippingCartLineInfo,
    ICheckoutShippingCartLinesProps
} from './components/checkout-shipping-cartlines-images';

/**
 * The checkout address props interface.
 */
export interface ICheckoutAddressProps extends ICustomCheckoutShippingAddressProps<ICustomCheckoutShippingAddressData>, IModuleStateProps {}

/**
 * The checkout shipping address view state.
 */
export interface ICheckoutShippingAddressViewState {
    isShowAddress: boolean;
    isShowAddresList: boolean;
    isShowAddOrUpdateAddress: boolean;
}

/**
 * The checkout shipping address state.
 */
export interface ICheckoutAddresState {
    shippingGroups: ICartLineWithProduct[];
    deliveryNotes: string;
}

/**
 * This setting defines DELIVERYNOTES Key.
 */
export const deliveryNoteKey = 'DELIVERYNOTES';

/**
 * The checkout shipping address view props.
 */
export interface ICheckoutShippingAddressViewProps extends ICheckoutAddressProps {
    className: string;
    currentOperation: AddressOperation;
    selectedAddress?: Address;
    addUpdateAddress: Address;
    addressListSelectedAddress: Address;
    countryRegionId: string;
    stateProvinceInfo?: StateProvinceInfo[];
    customerAddresses: Address[];
    validationError: object;
    addressActionResponse?: IAddressResponse;
    viewState: ICheckoutShippingAddressViewState;
    CheckoutShippingAddress: IModuleProps;
    isUpdating: boolean;
    hasError: boolean;
    showAddress: IAddressShowProps;
    showAddOrUpdateAddress: IAddressAddUpdateProps;
    cartLineImages?: React.ReactNode;
    showAddressSelect: IAddressSelectProps;
    deliveryNotes?: JSX.Element;
    showAddOrUpdateAddressHandler?(onSaveHandler?: () => void, onCancelHandler?: () => void): IAddressAddUpdateProps;
    showAddressSelectHandler?(
        onAddAddressHandler?: () => void,
        onSaveHandler?: () => void,
        onCancelHandler?: () => void
    ): IAddressSelectProps;
}

/**
 * IExpressPaymentDetail interface.
 */
interface IExpressPaymentDetails {
    email?: string;
    tenderLine?: TenderLine;
    tokenizedPaymentCard?: TokenizedPaymentCard;
    paymentTenderType?: string;
    address?: Address;
    isExpressCheckoutAppliedInCartPage: boolean;
}

/**
 *
 * Address component.
 * @extends {React.Component<ICustomCheckoutShippingAddressProps<ICustomCheckoutShippingAddressData>>}
 */
// @ts-expect-error
@withModuleState
@observer
class CheckoutShippingAddress extends React.Component<ICheckoutAddressProps, ICheckoutAddresState> {
    @observable private currentOperation: AddressOperation;

    @observable private selectedAddress?: Address;

    @observable private addUpdateAddress: Address;

    @observable private countryRegionId: string = 'USA';

    @observable private stateProvinceInfo?: StateProvinceInfo[];

    @observable private customerAddresses: Address[] = [];

    @observable private validationError: object;

    @observable private addressActionResponse?: IAddressResponse;

    @observable private isUpdating?: boolean;

    @observable private hasError?: boolean;

    @observable private addressListSelectedAddress: Address = {};

    private readonly addressCommon: AddressCommon;

    private addressFormat!: AddressFormat;

    private countryRegions: CountryRegionInfo[] = [];

    private addressPurposes: AddressPurpose[] = [];

    private readonly resources: IAddressResource;

    private readonly defaultAddressType: number = 6; // Default to Home

    private readonly telemetryContent?: ITelemetryContent;

    private autoSuggest?: AutoSuggest;

    private readonly multiplePickupStoreSwitchName: string = 'Dynamics.AX.Application.RetailMultiplePickupDeliveryModeFeature';

    private retailMultiplePickUpOptionEnabled?: boolean = false;

    private deliveryNotesRef: React.RefObject<HTMLTextAreaElement>;

    public constructor(props: ICheckoutAddressProps) {
        super(props);
        this.state = { shippingGroups: [], deliveryNotes: this._getAttributeValue(deliveryNoteKey) || '' };
        const { context, data, resources, telemetry } = this.props;

        this.addUpdateAddress = {};
        this.resources = resources;
        this.currentOperation = AddressOperation.List;
        this.countryRegions = data.countryRegions.result || [];
        this.addressPurposes = data.addressPurposes.result || [];
        this.customerAddresses = data.address.result || [];
        this.stateProvinceInfo = data.countryStates.result || [];
        this.addressCommon = new AddressCommon(context, resources, telemetry);
        this.addressFormat = new AddressFormat(
            this.countryRegions,
            new AddressMetaData({ ...resources }, this._getAddressFormatExcludeList()),
            this.addressPurposes
        );
        this.validationError = {};
        this.retailMultiplePickUpOptionEnabled = data.featureState.result?.find(
            feature => feature.Name === this.multiplePickupStoreSwitchName
        )?.IsEnabled;
        this.telemetryContent = getTelemetryObject(
            this.props.context.request.telemetryPageName!,
            this.props.friendlyName,
            this.props.telemetry
        );
        this.deliveryNotesRef = React.createRef();
    }

    /**
     * Initialize pickup group.
     */
    @action
    private readonly _initPickupGroup = async (): Promise<void> => {
        const pickupCartLines: CartLine[] = this._getCartLinesforDelivery();
        const cartLines: ICartLineWithProduct[] = [];

        try {
            const products = await this._getProductsByCartLines(
                this.props.data.checkout.result?.checkoutCart.cart.ChannelId || 0,
                pickupCartLines
            );
            for (const line of pickupCartLines) {
                const product: SimpleProduct | undefined = products.find(x => x.RecordId === line.ProductId);
                cartLines.push({ cartLine: line, product });
            }
            this.setState({ shippingGroups: cartLines });
        } catch (error) {
            this.props.telemetry.error(error);
            this.setState({ shippingGroups: [] });
        }
    };

    /**
     * On suggestion selected.
     * @param result - Suggestion result interface.
     */
    @action
    private readonly _onSuggestionSelected = async (result: Microsoft.Maps.ISuggestionResult): Promise<void> => {
        this._clearAddressFields();
        const address = this.addressFormat.getTranformedAddress(result, this.stateProvinceInfo);
        const timeout = 0;
        set(this.addUpdateAddress, { Street: '' });
        set(this.addUpdateAddress, { ZipCode: address.ZipCode });
        set(this.addUpdateAddress, { CountyName: address.CountyName });
        set(this.addUpdateAddress, { City: address.City });
        set(this.addUpdateAddress, { State: address.State });
        set(this.addUpdateAddress, { DistrictName: address.DistrictName });
        set(this.addUpdateAddress, { FullAddress: address.FullAddress });

        // Bing autosuggest put the complete address in the Street input box. Updating the street input box to show only street address.
        setTimeout(() => {
            set(this.addUpdateAddress, { Street: address.Street });
        }, timeout);
    };

    /**
     * Method to clear address fields.
     */
    @action
    private readonly _clearAddressFields = (): void => {
        const addressFormatItem = this.addressFormat.getAddressFormat(
            this.addUpdateAddress.ThreeLetterISORegionName || this.countryRegionId
        );
        for (const formatAddress of addressFormatItem) {
            if (
                this.addUpdateAddress[formatAddress.name] !== undefined &&
                !this.autoSuggest?.excludedAddressFields.includes(formatAddress.name)
            ) {
                this.addressFormat[formatAddress.name] = '';
            }
        }
        this._clearValidation();
    };

    /**
     * Method to clear validation.
     */
    @action
    private readonly _clearValidation = (): void => {
        this.validationError = {};
    };

    public get expressPaymentDetailsFromCartPage(): IExpressPaymentDetails | null {
        // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Explicitly check for null/undefined.
        const properties =
            this.props.data.cart?.result?.cart?.ExtensionProperties?.find(property => property.Key === 'expressPaymentDetails')?.Value
                ?.StringValue ?? '';

        return properties ? (JSON.parse(properties) as IExpressPaymentDetails) : null;
    }

    public async componentDidMount(): Promise<void> {
        const {
            context: {
                telemetry,
                actionContext: {
                    requestContext: { channel }
                }
            },
            config: { autoSuggestionEnabled, autoSuggestOptions },
            resources
        } = this.props;

        // Initializing data props
        this._dataInitialize(this.props);

        this.addressFormat = new AddressFormat(
            this.countryRegions,
            new AddressMetaData({ ...resources }, this._getAddressFormatExcludeList()),
            this.addressPurposes
        );

        this.props.data.checkout.then(() => {
            this._setDefaultCountryRegionId();
            this._initModuleState();
        });

        reaction(
            () => this.countryRegionId,
            () => {
                this._getStateProvinces();
            }
        );

        reaction(
            () => this.currentOperation,
            () => {
                this._getStateProvinces();
            }
        );

        reaction(
            () => this.props.data.checkout.result?.shippingAddressFromExpressCheckout,
            () => {
                if (!this.expressPaymentDetailsFromCartPage && this._canShip()) {
                    let address = this.props.data.checkout.result?.shippingAddressFromExpressCheckout;

                    if (address?.TwoLetterISORegionName) {
                        const threeLetterIsoRegionName = this.getThreeLetterIsoRegionName(address.TwoLetterISORegionName);

                        address = { ...address, ThreeLetterISORegionName: threeLetterIsoRegionName };

                        if (threeLetterIsoRegionName && threeLetterIsoRegionName !== this.countryRegionId) {
                            this._onCountryChange(threeLetterIsoRegionName);
                        }
                    }

                    this._updateCurrentOperation(AddressOperation.Add, address);
                    this._onAddressAddUpdateSubmit();
                }
            }
        );

        if (autoSuggestionEnabled) {
            if (channel && !channel.BingMapsApiKey) {
                telemetry.error('BingMapsApiKey is missing.');
                return;
            }

            if (channel && !channel.BingMapsEnabled) {
                telemetry.error('Map is disabled from HQ.');
                return;
            }

            const options: IAutoSuggestOptions = { ...autoSuggestOptions };
            this.autoSuggest = new AutoSuggest(
                telemetry,
                options,
                channel?.BingMapsApiKey,
                channel?.ChannelCountryRegionISOCode,
                channel?.DefaultLanguageId
            );

            // Customer doesn't have any address. Then add view will be loaded directly. Code for the same to handle that
            if (
                this.props.data.storeSelectorStateManager.result &&
                (this.currentOperation === AddressOperation.Add || this.currentOperation === AddressOperation.Update)
            ) {
                await this.autoSuggest._loadMapAPI(await this.props.data.storeSelectorStateManager);
            }

            reaction(
                () =>
                    this.props.data.storeSelectorStateManager.result?.loadMapApi &&
                    (this.currentOperation === AddressOperation.Add || this.currentOperation === AddressOperation.Update),
                async () => {
                    await this.autoSuggest?._loadMapAPI(await this.props.data.storeSelectorStateManager);
                }
            );

            reaction(
                () => this.props.data.storeSelectorStateManager.result?.isMapApiLoaded,
                async () => {
                    await this._attachMapAutoSuggest();
                }
            );
        }
        await this._initPickupGroup();
    }

    public async componentDidUpdate(): Promise<void> {
        if (this.currentOperation === AddressOperation.Add || this.currentOperation === AddressOperation.Update) {
            if (this.props.data.storeSelectorStateManager.result?.isMapApiLoaded) {
                await this._attachMapAutoSuggest();
            }
        } else {
            this.autoSuggest?.disposeAutoSuggest();
        }
    }

    public shouldComponentUpdate(nextProps: ICheckoutAddressProps, nextState: ICheckoutAddresState): boolean {
        if (this.state === nextState && this.props.data === nextProps.data) {
            return false;
        }
        return true;
    }

    public render(): JSX.Element | null {
        if (!this._canShip()) {
            return null;
        }

        const { config, context, renderView, resources } = this.props;
        const { headingImages, itemsText, singleItemText } = resources;
        const { imageSettings } = config;

        // Line images
        const cartlines: ICheckoutShippingCartLineInfo[] = this.state.shippingGroups
            .filter(x => !ObjectExtensions.isNullOrUndefined(x.product))
            .map(line => ({
                lineId: line.cartLine.LineId ?? '',
                imageProps: {
                    requestContext: context.actionContext.requestContext,
                    className: 'ms-checkout-shipping-address__group-images-lines-line-image',
                    altText: line.product?.Name,
                    src: line.product?.PrimaryImageUrl ?? '',
                    fallBackSrc: getFallbackImageUrl(line.product?.ItemId, context.actionContext.requestContext.apiSettings),
                    gridSettings: context.request.gridSettings!,
                    imageSettings: imageSettings ?? defaultImageSettings,
                    loadFailureBehavior: 'empty'
                },
                quantity: line.cartLine.Quantity ?? 0
            }));

        const quantity = 1;
        const itemText = cartlines.length > quantity ? itemsText : singleItemText;

        const lineImageProps: ICheckoutShippingCartLinesProps = {
            moduleClassName: 'ms-checkout-shipping-address',
            cartLines: cartlines.filter(x => !ObjectExtensions.isNullOrUndefined(x)),
            itemTitle: `(${format(itemText, cartlines.length)})`,
            title: headingImages,
            resources: this.props.resources
        };

        const cartLineImages: React.ReactNode = <CheckoutPickupCartLines {...lineImageProps} />;
        const multiplePickupStoreSwitchName = 'Dynamics.AX.Application.RetailMultiplePickupDeliveryModeFeature';
        const { featureState } = this.props.data;
        const isRetailMultiplePickUpOptionEnabled = featureState.result?.find(feature => feature.Name === multiplePickupStoreSwitchName)
            ?.IsEnabled;

        const viewProps = {
            ...this.props,
            currentOperation: this.currentOperation,
            selectedAddress: this.selectedAddress,
            addUpdateAddress: this.addUpdateAddress,
            addressListSelectedAddress: this.addressListSelectedAddress,
            countryRegionId: this.countryRegionId,
            stateProvinceInfo: this.stateProvinceInfo,
            customerAddresses: this.customerAddresses,
            validationError: this.validationError,
            addressActionResponse: this.addressActionResponse,
            isUpdating: this.isUpdating,
            hasError: this.hasError,
            className: config.className,
            viewState: {
                isShowAddress: this.currentOperation === AddressOperation.Show && this.selectedAddress,
                isShowAddresList: this.currentOperation === AddressOperation.List && this.customerAddresses.length > 0,
                isShowAddOrUpdateAddress:
                    this.currentOperation === AddressOperation.Add || this.currentOperation === AddressOperation.Update
            },
            CheckoutShippingAddress: {
                moduleProps: this.props,
                className: classnames('ms-checkout-shipping-address', config.className)
            },
            showAddress: this._renderShowAddress(),

            /**
             * Show address select.
             * @param onAddAddressHandler - On add address click function.
             * @param onSaveHandler - On save click function.
             * @param onCancelHandler - On cancel click function.
             * @returns - Renders select address.
             */
            showAddressSelectHandler: (onAddAddressHandler?: () => void, onSaveHandler?: () => void, onCancelHandler?: () => void) =>
                this._renderSelectAddress(onAddAddressHandler, onSaveHandler, onCancelHandler),

            /**
             * Show add/update address.
             * @param onSaveHandler - On save click function.
             * @param onCancelHandler - On cancel click function.
             * @returns - Renders select address.
             */
            showAddOrUpdateAddressHandler: (onSaveHandler?: () => void, onCancelHandler?: () => void) =>
                this._renderAddOrUpdateAddress(onSaveHandler, onCancelHandler),
            showAddOrUpdateAddress: this._renderAddOrUpdateAddress(),
            cartLineImages: isRetailMultiplePickUpOptionEnabled ? cartLineImages : undefined,
            showAddressSelect: this._renderSelectAddress(),
            deliveryNotes: this._renderDeliveryNotes()
        };

        return renderView(viewProps) as React.ReactElement;
    }

    /**
     * Method to render delivery notes.
     */
    private _renderDeliveryNotes = (): JSX.Element => {
        const { deliveryNotes } = this.state;
        return (
            <div className={'delivery-notes'}>
                {(this.currentOperation === AddressOperation.Add || this.currentOperation === AddressOperation.Update) && (
                    <textarea
                        ref={this.deliveryNotesRef}
                        aria-label='Delivery Notes'
                        placeholder={this.props.resources.deliveryNotesPlaceHolderText}
                        className='deliveryNotes-textarea'
                        value={deliveryNotes}
                        onChange={this._onEditDeliveryNotes}
                    />
                )}
                {this.currentOperation === AddressOperation.Show && (
                    <div className='checkout-delivery-options__deliveryNotes'>{deliveryNotes}</div>
                )}
            </div>
        );
    };

    /**
     * Method _getExtensionPropertyValue.
     * @param fieldName -The checkout address properties.
     */
    private _getAttributeValue = (fieldName: string) => {
        const deliveryNotes =
            this.props.data.checkout.result &&
            this.props.data.checkout.result.checkoutCart &&
            this.props.data.checkout.result.checkoutCart.cart &&
            this.props.data.checkout.result.checkoutCart.cart?.AttributeValues?.find(attribute => attribute.Name === fieldName);

        if (deliveryNotes) {
            // @ts-expect-error -- Need to provide data type.
            return deliveryNotes?.TextValue;
        }
        return undefined;
    };

    /**
     * Method _getExtensionPropertyValue.
     * @param event -The checkout address properties.
     */
    private _onEditDeliveryNotes = (event: React.ChangeEvent<HTMLTextAreaElement>): void => {
        const textArea: HTMLTextAreaElement | null =
            this.deliveryNotesRef && this.deliveryNotesRef.current && this.deliveryNotesRef.current;
        const deliveryNotes = (this.deliveryNotesRef && this.deliveryNotesRef.current && this.deliveryNotesRef.current.value) || '';
        const text: string = deliveryNotes;
        textArea!.value = text;
        this.setState({ deliveryNotes: text });
    };

    /**
     * Method data initialization.
     * @param props -The checkout address properties.
     */
    private readonly _dataInitialize = (props: ICheckoutAddressProps): void => {
        const { data } = props;

        reaction(
            () => data.countryRegions.result,
            () => {
                this.countryRegions = data.countryRegions.result ?? [];
            }
        );

        reaction(
            () => data.addressPurposes.result,
            () => {
                this.addressPurposes = data.addressPurposes.result ?? [];
            }
        );

        reaction(
            () => data.address.result,
            () => {
                this.customerAddresses = data.address.result ?? [];
            }
        );

        reaction(
            () => data.countryStates.result,
            () => {
                this.stateProvinceInfo = data.countryStates.result ?? [];
            }
        );

        reaction(
            () => data.featureState.result,
            () => {
                this.retailMultiplePickUpOptionEnabled = data.featureState.result?.find(
                    feature => feature.Name === this.multiplePickupStoreSwitchName
                )?.IsEnabled;
            }
        );
    };

    /**
     * Method to get cart lines for delivery.
     * @returns The cart line collection.
     */
    private readonly _getCartLinesforDelivery = (): CartLine[] => {
        return this.props.data.checkout.result?.checkoutCart.cart.CartLines?.filter(line => this._isDelivery(line)) ?? [];
    };

    /**
     * Method to check cart line for delivery.
     * @param line -The cart line.
     * @returns True/false as per cart line delivery mode.
     */
    private readonly _isDelivery = (line: CartLine): boolean => {
        return this._isNotPickupMode(line.DeliveryMode) && (line.FulfillmentStoreId === undefined || line.FulfillmentStoreId === '');
    };

    /**
     * Method to check cart line for delivery.
     * @param deliveryMode -The delivery mode.
     * @returns True/false as per cart line delivery mode.
     */
    private readonly _isNotPickupMode = (deliveryMode: string | undefined): boolean => {
        const pickupDeliveryModeCode = this.props.context.request.channel?.PickupDeliveryModeCode;
        const multiplePickupStoreSwitchName = 'Dynamics.AX.Application.RetailMultiplePickupDeliveryModeFeature';
        const { channelDeliveryOptionConfig, featureState } = this.props.data;
        const retailMultiplePickUpOptionEnabled = featureState.result?.find(feature => feature.Name === multiplePickupStoreSwitchName)
            ?.IsEnabled;
        if (retailMultiplePickUpOptionEnabled && deliveryMode !== undefined) {
            const pickupDeliveryMode = channelDeliveryOptionConfig.result?.PickupDeliveryModeCodes?.some(
                pickupMode => pickupMode !== deliveryMode
            );
            return pickupDeliveryMode !== undefined ? pickupDeliveryMode : false;
        }
        return deliveryMode !== undefined && pickupDeliveryModeCode !== undefined && deliveryMode !== pickupDeliveryModeCode;
    };

    /**
     * Method to check cart line for delivery.
     * @param channelId - The channelId.
     * @param cartLines - Cart line collections.
     * @returns Collection of SimpleProduct.
     */
    private readonly _getProductsByCartLines = async (channelId: number, cartLines: CartLine[]): Promise<SimpleProduct[]> => {
        const actionContext = this.props.context.actionContext;
        const productInputs = cartLines
            .filter(line => !ObjectExtensions.isNullOrUndefined(line.ProductId))
            .map(
                line =>
                    new ProductInput(
                        line.ProductId || 0,
                        actionContext.requestContext.apiSettings,
                        channelId,
                        undefined,
                        actionContext.requestContext
                    )
            );
        return getSimpleProducts(productInputs, actionContext);
    };

    /**
     * Method to render add/update address.
     * @param onSaveHandler -- Handles onsave functionality.
     * @param onCancelHandler -- Handles oncancel functionality.
     * @returns Address app/update props.
     */
    private readonly _renderAddOrUpdateAddress = (onSaveHandler?: () => void, onCancelHandler?: () => void): IAddressAddUpdateProps => {
        const addressFormat =
            this.currentOperation === AddressOperation.Add && this.addressCommon.isAuthenticatedFlow()
                ? this.addressFormat
                : this.addressFormat;

        /**
         * On Cancel Button Function.
         */
        const onCancelButtonHandler = () => {
            this._resetView();
            onCancelHandler?.();
        };
        return AddressAddUpdate({
            isUpdating: this.isUpdating,
            resources: this.resources,
            addressType: AddressType.Shipping,
            addressFormat: addressFormat.getAddressFormat(this.addUpdateAddress.ThreeLetterISORegionName || this.countryRegionId),
            defaultCountryRegionId: this.countryRegionId,
            defaultAddressType: this.defaultAddressType,
            selectedAddress: this.addUpdateAddress,
            validationError: this.validationError,
            hasError: this.hasError,
            addressActionResponse: this.addressActionResponse,
            telemetryContent: this.telemetryContent,
            dropdownDisplayData: addressFormat.getPrefilledAddressDropdownData(
                this.resources.addressStateDefaultSelectionText,
                this.stateProvinceInfo
            ),
            onInputChange: this._onAddressAddUpdateInputChange,
            onDropdownChange: this._onAddressAddUpdateDropdownChange,
            hasExternalSubmitGroup: this.props.moduleState.hasExternalSubmitGroup,

            /**
             * On Save Function.
             */
            onSave: () => {
                this.onSubmit();
                onSaveHandler?.();
            },

            onCancel: !this.selectedAddress && !this.addressCommon.isAuthenticatedFlow() ? () => {} : onCancelButtonHandler
        });
    };

    /**
     * Method to render select address.
     * @param onAddAddressHandler - To handle add address button click.
     * @param onSaveHandler - To handle save button click.
     * @param onCancelHandler - To handle cancel button click.
     * @returns Select address props.
     */
    private readonly _renderSelectAddress = (
        onAddAddressHandler?: () => void,
        onSaveHandler?: () => void,
        onCancelHandler?: () => void
    ): IAddressSelectProps => {
        /**
         * On Cancel Button Function.
         */
        const onCancelButtonHandler = () => {
            this._resetView();
            onCancelHandler?.();
        };
        return AddressSelect({
            addressFormat: this.addressFormat,
            addresses: this.customerAddresses,
            resources: this.resources,
            addressPurposes: this.addressPurposes,
            selectedAddress: this.addressListSelectedAddress,
            onAddressOptionChange: this._onAddressOptionChange,
            hasExternalSubmitGroup: this.props.moduleState.hasExternalSubmitGroup,
            telemetryContent: this.telemetryContent,

            /**
             * On Add Address Function.
             */
            onAddAddress: () => {
                this._goToAddAddress();
                onAddAddressHandler?.();
            },

            /**
             * On Save Function.
             */
            onSave: () => {
                this._onSelectAddress();
                onSaveHandler?.();
            },

            onCancel: !this.selectedAddress ? () => {} : onCancelButtonHandler
        });
    };

    /**
     * Method to render show address.
     * @returns Show address props.
     */
    private _renderShowAddress(): IAddressShowProps | null {
        if (this.selectedAddress) {
            return AddressShow({
                address: this.selectedAddress,
                addressFormat: this.addressFormat.getAddressFormat(this.selectedAddress.ThreeLetterISORegionName || ''),
                addressPurposes: this.addressPurposes
            });
        }

        return null;
    }

    /**
     * Method to render map auto suggest.
     */
    private readonly _attachMapAutoSuggest = async (): Promise<void> => {
        const {
            data: {
                storeSelectorStateManager: { result: storeSelectorStateManager }
            }
        } = this.props;

        if (storeSelectorStateManager?.isMapApiLoaded) {
            this.autoSuggest?.attachAutoSuggest('#shipping_addressstreet', '#shipping_addressstreet_container', this._onSuggestionSelected);
        }
    };

    /**
     * Method gets called on address option change.
     * @param event - To get current option.
     */
    private readonly _onAddressOptionChange = (event: React.ChangeEvent<HTMLInputElement>) => {
        const addressRecordId = event.currentTarget.value;
        const selectedAddress = this.customerAddresses.find(address => (address.RecordId || '').toString() === addressRecordId);
        if (selectedAddress) {
            this.addressListSelectedAddress = selectedAddress;
        }
    };

    /**
     * Method to render map auto suggest.
     * @param name - Address name field.
     * @param value - Address name value field.
     */
    private readonly onAddressAddUpdate = (name: string, value: string | boolean) => {
        set(this.addUpdateAddress, { [name]: value });
        this.addressFormat.validateAddressFormat(this.addUpdateAddress, this.validationError, this.countryRegionId, name);
    };

    /**
     * Method to get called on address update change.
     * @param event - Input element.
     */
    private readonly _onAddressAddUpdateInputChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
        if (event.target.type === 'checkbox') {
            this.onAddressAddUpdate(event.target.name, event.target.checked);
        } else {
            const value = (event.target.value || '').replace(new RegExp('[<>]', 'gi'), '');
            this.onAddressAddUpdate(event.target.name, value);
        }
    };

    /**
     * Method to get called on address update dropdown change.
     * @param event - Select element.
     */
    private readonly _onAddressAddUpdateDropdownChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
        this.onAddressAddUpdate(event.target.name, event.target.value);

        if (event.target.name === AddressItemType[AddressItemType.ThreeLetterISORegionName]) {
            this._onCountryChange(event.target.value);
        }
    };

    /**
     * Method to get called on address update submit.
     */
    private readonly _onAddressAddUpdateSubmit = (): void => {
        if (!this.addressFormat.validateAddressFormat(this.addUpdateAddress, this.validationError, this.countryRegionId)) {
            return;
        }

        let response: Promise<IAddressResponse>;
        if (this.addressCommon.isAuthenticatedFlow()) {
            response =
                this.currentOperation === AddressOperation.Update
                    ? this.addressCommon.updateCustomerAddress(this.addUpdateAddress)
                    : this.addressCommon.addCustomerAddress(this.addUpdateAddress);
        } else {
            response = Promise.resolve({ address: this.addUpdateAddress });
        }

        this.isUpdating = true;
        this._updateModuleState();

        response.then(
            (result: IAddressResponse) => {
                this.isUpdating = false;

                let newAddress = result.address;
                newAddress = { ...newAddress, ...this.addUpdateAddress };

                if (result.address) {
                    this.hasError = false;
                    if (result.customerAddresses) {
                        this._onAddOrUpdateSuccess({ customerAddresses: result.customerAddresses, address: newAddress });
                    } else {
                        this._onAddOrUpdateSuccess({ address: newAddress });
                    }
                } else {
                    this.hasError = true;
                    this.addressActionResponse = result;
                    this._updateModuleState();
                }
            },
            () => {
                this.hasError = true;
                this.isUpdating = false;
                this._updateModuleState();
            }
        );
    };

    /**
     * Method to get called on country change.
     * @param countryRegionId - Country region id.
     */
    private readonly _onCountryChange = (countryRegionId: string) => {
        this.countryRegionId = countryRegionId;
        const twoLetterIsoRegionName = this.addressFormat.getTwoLetterISORegionName(countryRegionId);
        set(this.addUpdateAddress, { ThreeLetterISORegionName: countryRegionId });
        set(this.addUpdateAddress, { TwoLetterISORegionName: twoLetterIsoRegionName });
        this.autoSuggest?.changeAutoSuggestionCountryCode(twoLetterIsoRegionName);
        this._clearAddressFields();
    };

    /**
     * Method to get all address format exclude list.
     * @returns Collection of address items.
     */
    private readonly _getAddressFormatExcludeList = (): AddressItemType[] => {
        const { config } = this.props;
        const addressFormatExcludeList: AddressItemType[] = [];

        if (!config.showAddressType) {
            addressFormatExcludeList.push(AddressItemType.AddressTypeValue);
        }

        addressFormatExcludeList.push(AddressItemType.IsPrimary);

        return addressFormatExcludeList;
    };

    /**
     * Method to get all state/provinces.
     */
    private readonly _getStateProvinces = (): void => {
        if (
            !this.countryRegionId ||
            !(this.currentOperation === AddressOperation.Add || this.currentOperation === AddressOperation.Update)
        ) {
            return;
        }

        this.addressCommon.getStateProvinces(this.countryRegionId).then((result: StateProvinceInfo[]) => {
            const stateInfo = result.some(state => state.StateId === this.addUpdateAddress.State);

            // Reset state if selected state not found in the list.
            if (!stateInfo) {
                set(this.addUpdateAddress, { State: '' });
            }

            this.stateProvinceInfo = result;
        });
    };

    /**
     * Method to set default country region id.
     */
    private _setDefaultCountryRegionId(): void {
        const { request } = this.props.context;
        const market = request.channel?.ChannelCountryRegionISOCode;
        this.countryRegionId = this.addressCommon.getDefaultCountryRegionId(this.countryRegionId, this.countryRegions, market);
    }

    /**
     * Method to get default address region id.
     * @returns - Address object from existing addresses.
     */
    private readonly _getDefaultAddress = (): Address | undefined => {
        if (this.customerAddresses) {
            const primaryAddress = this.customerAddresses.find((address: Address) => address.IsPrimary);
            return primaryAddress || (this.customerAddresses.length > 0 ? this.customerAddresses[0] : undefined);
        }
        return undefined;
    };

    /**
     * Method to get address from express payment details from cart page.
     * @returns - Addresss.
     */
    private readonly _getAddressFromCartExpressPaymentDetails = (): Address | undefined => {
        if (this.expressPaymentDetailsFromCartPage) {
            const { tokenizedPaymentCard } = this.expressPaymentDetailsFromCartPage;
            const address = tokenizedPaymentCard ? this.getAddressFromTokenizedPaymentCard(tokenizedPaymentCard) : undefined;
            return address;
        }
        return undefined;
    };

    /**
     * Get address from tokenizedPaymentCard.
     * @param tokenizedPaymentCard -- The tokenizedPaymentCard from the payment.
     * @returns The address.
     */
    private readonly getAddressFromTokenizedPaymentCard = (tokenizedPaymentCard: TokenizedPaymentCard): Address => {
        const twoLetterIsoRegionName = tokenizedPaymentCard.Country;

        const threeLetterIsoRegionName = twoLetterIsoRegionName ? this.getThreeLetterIsoRegionName(twoLetterIsoRegionName) : undefined;

        if (threeLetterIsoRegionName && threeLetterIsoRegionName !== this.countryRegionId) {
            this._onCountryChange(threeLetterIsoRegionName);
        }

        const address: Address = {
            TwoLetterISORegionName: twoLetterIsoRegionName,
            Name: tokenizedPaymentCard.NameOnCard,
            Street: tokenizedPaymentCard.Address1,
            StreetNumber: tokenizedPaymentCard.Address2,
            City: tokenizedPaymentCard.City,
            State: tokenizedPaymentCard.State,
            ZipCode: tokenizedPaymentCard.Zip,
            Phone: tokenizedPaymentCard.Phone,
            ThreeLetterISORegionName: threeLetterIsoRegionName,
            AddressTypeValue: this.defaultAddressType
        };

        return address;
    };

    /**
     * Get three letter ISO region name from two letter ISO region name.
     * @param twoLetterIsoRegionName -- The three letter ISO region name.
     * @returns The three letter ISO region name.
     */
    private readonly getThreeLetterIsoRegionName = (twoLetterIsoRegionName: string): string | undefined => {
        const countryRegion = this.countryRegions.find(country => {
            return country.ISOCode?.toLowerCase() === twoLetterIsoRegionName.toLowerCase();
        });

        return countryRegion?.CountryRegionId;
    };

    /**
     * Method to initialize all module state.
     */
    private readonly _initModuleState = (): void => {
        this.props.moduleState.init({
            status: this._canShip() ? 'updating' : 'disabled',
            onEdit: this.onEdit,
            onCancel: this.onCancel,
            onSubmit: this.onSubmit
        });

        const checkoutState = this.props.data.checkout.result;

        if (this._canShip()) {
            let defaultAddress;

            if (checkoutState?.checkoutCart.cart.ShippingAddress) {
                defaultAddress = checkoutState.checkoutCart.cart.ShippingAddress;
            } else if (this._getAddressFromCartExpressPaymentDetails()) {
                defaultAddress = this._getAddressFromCartExpressPaymentDetails();
            } else if (this._getDefaultAddress()) {
                defaultAddress = this._getDefaultAddress();
            } else {
                defaultAddress = checkoutState?.isExpressCheckoutApplied ? checkoutState.shippingAddressFromExpressCheckout : undefined;
            }

            if (defaultAddress && !this.addressCommon.isEmpty(defaultAddress)) {
                this._updateCurrentOperation(AddressOperation.Show, defaultAddress);
                this._setShippingAddress(defaultAddress, true);
                this._updateModuleState();
            } else {
                this._updateCurrentOperation(AddressOperation.Add);
            }
        }
    };

    /**
     * Method get called on submit address.
     */
    private readonly onSubmit = (): void => {
        switch (this.currentOperation) {
            case AddressOperation.Add:
            case AddressOperation.Update:
                this._onAddressAddUpdateSubmit();
                break;
            case AddressOperation.List:
                this._onSelectAddress();
                break;
            default:
                this.props.telemetry.error('Invalid operation');
        }
    };

    /**
     * Method get called on cancel.
     */
    private readonly onCancel = (): void => {
        switch (this.currentOperation) {
            case AddressOperation.Add:
            case AddressOperation.Update:
                this._clearAddressFields();
                if (!(!this.selectedAddress && !this.addressCommon.isAuthenticatedFlow())) {
                    this.setState({
                        deliveryNotes: this._getAttributeValue(deliveryNoteKey) || ''
                    });
                    this._resetView();
                }

                break;
            case AddressOperation.List:
                if (this.selectedAddress) {
                    this.setState({
                        deliveryNotes: this._getAttributeValue(deliveryNoteKey) || ''
                    });
                    this._resetView();
                }
                break;
            default:
                this.props.telemetry.error('Invalid operation');
        }
    };

    /**
     * Method get called on edit address.
     */
    private readonly onEdit = (): void => {
        if (this.addressCommon.isAuthenticatedFlow() && this.shippingAddress) {
            this._updateCurrentOperation(AddressOperation.List, this.shippingAddress);
        } else if (this.shippingAddress) {
            this._updateCurrentOperation(AddressOperation.Update, this.shippingAddress);
        }

        this._updateModuleState();
    };

    /**
     * Method to check if checkout cartlines are available fro shipping.
     * @returns - True/false as per the delivery mode.
     */
    private readonly _canShip = (): boolean => {
        const { checkout, channelDeliveryOptionConfig } = this.props.data;
        const { request } = this.props.context;
        const pickupDeliveryModeCode = request && request.channel && request.channel.PickupDeliveryModeCode;
        const emailDeliveryModeCode = request && request.channel && request.channel.EmailDeliveryModeCode;
        if (!checkout.result || !request.channel || checkout.result.checkoutCart.isEmpty || checkout.result.checkoutCart.hasInvoiceLine) {
            return false;
        }

        // @ts-expect-error: Type-checker not realizing above request.channel check
        return this.retailMultiplePickUpOptionEnabled
            ? checkout.result.checkoutCart.cart.CartLines?.some(cartLine =>
                  cartLine.DeliveryMode && cartLine.DeliveryMode !== ''
                      ? cartLine.DeliveryMode !==
                            channelDeliveryOptionConfig.result?.PickupDeliveryModeCodes?.find(
                                deliveryMode => deliveryMode === cartLine.DeliveryMode
                            ) && cartLine.DeliveryMode !== emailDeliveryModeCode
                      : cartLine
              )
            : checkout.result.checkoutCart.cart.CartLines?.some(cartLine =>
                  cartLine.DeliveryMode && cartLine.DeliveryMode !== ''
                      ? cartLine.DeliveryMode !== pickupDeliveryModeCode && cartLine.DeliveryMode !== emailDeliveryModeCode
                      : cartLine
              );
    };

    /**
     * Method get called on select address.
     */
    private readonly _onSelectAddress = () => {
        this._updateCurrentOperation(AddressOperation.Show, this.addressListSelectedAddress);
        this._setShippingAddress(this.addressListSelectedAddress);
        this._updateModuleState();
    };

    /**
     * Method get called on goto add address.
     */
    private readonly _goToAddAddress = () => {
        this._setDefaultCountryRegionId();
        this._updateCurrentOperation(AddressOperation.Add);
    };

    /**
     * Method get called on add/update success.
     * @param response - Retail api response.
     */
    private readonly _onAddOrUpdateSuccess = (response: IAddressResponse) => {
        if (response.customerAddresses) {
            this.customerAddresses = response.customerAddresses;
        }

        if (response.address) {
            this._updateCurrentOperation(AddressOperation.Show, response.address);
            this._setShippingAddress(response.address);
            this._updateModuleState();
        }
    };

    /**
     * Method get called on set Shipping Address.
     * @param address - Retail Api address result.
     */
    private readonly _setShippingAddress = (address: Address, isInitMode: boolean = false): void => {
        if (this.props.data.checkout.result) {
            const newShippingAddress = { ...address };

            if (address.ThreeLetterISORegionName && !newShippingAddress.TwoLetterISORegionName) {
                newShippingAddress.TwoLetterISORegionName = this.addressFormat.getTwoLetterISORegionName(address.ThreeLetterISORegionName);
            }

            this.props.data.checkout.result.updateShippingAddress({ newShippingAddress });
            this.props.data.checkout.result.checkoutCart.updateShippingAddress({ newShippingAddress }).catch(error => {
                this.props.telemetry.error(error);
            });
            const { deliveryNotes } = this.state;
            this._updateAttributeValues(deliveryNoteKey, deliveryNotes).catch(error => {
                this.props.telemetry.error(`error in _updateExtensionProps ${error}`);
            });
        }
    };

    private readonly _updateAttributeValues = async (attributekey: string, value: string) => {
        await this.props.data.checkout.result?.checkoutCart.updateAttributeValues({
            newAttributeValues: [
                {
                    // @ts-expect-error -- Need to provide data type.
                    '@odata.type': '#Microsoft.Dynamics.Commerce.Runtime.DataModel.AttributeTextValue',
                    Name: attributekey,
                    TextValue: value,
                    ExtensionProperties: [],
                    TextValueTranslations: []
                }
            ]
        });
    };

    @computed private get shippingAddress(): Address | undefined {
        return this.props.data.checkout.result?.shippingAddress;
    }

    /**
     * Method get called on reset view.
     */
    private readonly _resetView = (): void => {
        switch (this.currentOperation) {
            case AddressOperation.Add:
            case AddressOperation.Update:
                this._updateCurrentOperation(
                    this.addressCommon.isAuthenticatedFlow() ? AddressOperation.List : AddressOperation.Show,
                    this.shippingAddress
                );
                break;
            default:
                this._updateCurrentOperation(AddressOperation.Show, this.shippingAddress);
        }
        this._updateModuleState();
    };

    /**
     * Update current operation.
     * @param operation - The address operation.
     * @param selectedAddress - The selected address.
     */
    private readonly _updateCurrentOperation = (operation: AddressOperation, selectedAddress?: Address) => {
        this.currentOperation = operation;
        this.selectedAddress = selectedAddress;

        if (this.currentOperation === AddressOperation.Add || this.currentOperation === AddressOperation.Update) {
            this.addUpdateAddress = { ...this.selectedAddress } || {};
            set(this.addUpdateAddress, {
                ThreeLetterISORegionName: this.addUpdateAddress.ThreeLetterISORegionName || this.countryRegionId
            });
            set(this.addUpdateAddress, {
                AddressTypeValue: this.addUpdateAddress.AddressTypeValue || this.defaultAddressType
            });
        } else if (this.currentOperation === AddressOperation.List) {
            this.addressListSelectedAddress = { ...this.selectedAddress } || {};
        }
    };

    /**
     * Update module state.
     */
    private readonly _updateModuleState = () => {
        if (this.currentOperation === AddressOperation.Show) {
            this.props.moduleState.onReady();
        } else if (this.isUpdating) {
            this.props.moduleState.onPending();
        } else {
            this.props.moduleState.onUpdating();
        }
    };
}

export default CheckoutShippingAddress;

4. Add logic to show delivery notes input in the custom-checkout-shipping-address.view.tsx

Implement the logic as shown below in the sample code to show delivery notes attribute into shipping address as delivery notes input box in src/modules/custom-checkout-shipping-address/custom-checkout-shipping-address.view.tsx.

/*--------------------------------------------------------------
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * See License.txt in the project root for license information.
 *--------------------------------------------------------------*/

/* eslint-disable no-duplicate-imports */
import { Module, Node } from '@msdyn365-commerce-modules/utilities';
import * as React from 'react';

import { IAddressAddItem, IAddressAddUpdateProps } from '@msdyn365-commerce-modules/address';
import { IAddressSelectItem, IAddressSelectProps } from '@msdyn365-commerce-modules/address';
import { IAddressShowItem, IAddressShowProps } from '@msdyn365-commerce-modules/address';
import { ICheckoutShippingAddressViewProps } from './custom-checkout-shipping-address';

/**
 * Address show component.
 * @param param0 - Root param.
 * @param param0.AddressDetail - Address detail.
 * @param param0.items - IAddressShowItem[].
 * @returns - Address Node.
 */
const AddressShow: React.FC<IAddressShowProps> = ({ AddressDetail, items }) => {
    return (
        <Node {...AddressDetail}>
            {items.map((item: IAddressShowItem) => {
                return <>{item.description}</>;
            })}
        </Node>
    );
};

/**
 * Address Select Component.
 * @param param0 - Root param.
 * @param param0.SelectAddress - Select address.
 * @param param0.addButton - Add button.
 * @param param0.items - IAddressSelectItem[].
 * @param param0.isShowSaveButton - Boolean.
 * @param param0.saveButton - Save button.
 * @param param0.isShowCancelButton - Boolean.
 * @param param0.cancelButton - Cancel button.
 * @returns - SelectAddress Node.
 */
const AddressSelect: React.FC<IAddressSelectProps> = ({
    SelectAddress,
    addButton,
    items,
    isShowSaveButton,
    saveButton,
    isShowCancelButton,
    cancelButton
}) => {
    return (
        <Node {...SelectAddress}>
            {addButton}
            {items.map((item: IAddressSelectItem) => {
                const SelectItem = item.SelectItem;
                return (
                    <Node {...SelectItem} key={item.key}>
                        {item.input}
                        <AddressShow {...item.showItems} />
                    </Node>
                );
            })}
            {isShowSaveButton && saveButton}
            {isShowCancelButton && cancelButton}
        </Node>
    );
};

/**
 * Address Add Update Component.
 * @param param0 - Root param.
 * @param param0.AddressForm - Address form.
 * @param param0.heading - Address heading.
 * @param param0.items - IAddressAddItem[].
 * @param param0.hasError - Boolean.
 * @param param0.error - Error message.
 * @param param0.isShowSaveButton - Boolean.
 * @param param0.saveButton - Save button.
 * @param param0.isShowCancelButton - Boolean.
 * @param param0.cancelButton - Cancel button.
 * @returns Address add update component node.
 */
const AddressAddUpdate: React.FC<IAddressAddUpdateProps> = ({
    AddressForm,
    heading,
    items,
    hasError,
    error,
    isShowSaveButton,
    saveButton,
    isShowCancelButton,
    cancelButton
}) => {
    return (
        <Node {...AddressForm}>
            {heading}
            {items.map((item: IAddressAddItem) => {
                const { AddressItem, key, label, alert, input } = item;
                return (
                    <Node {...AddressItem} key={key}>
                        {label}
                        {alert}
                        {input}
                    </Node>
                );
            })}
            {hasError && (
                <Node {...error.AddressError}>
                    {error.title}
                    {error.message}
                </Node>
            )}
            {isShowSaveButton && saveButton}
            {isShowCancelButton && cancelButton}
        </Node>
    );
};

/**
 * Checkout Shipping Address View Component.
 * @param props - Props.
 * @returns - CheckoutShippingAddress Module.
 */
const CheckoutShippingAddressView: React.FC<ICheckoutShippingAddressViewProps> = props => {
    const {
        CheckoutShippingAddress,
        viewState,
        showAddress,
        showAddressSelect,
        showAddOrUpdateAddress,
        cartLineImages,
        deliveryNotes
    } = props;

    return (
        <Module {...CheckoutShippingAddress}>
            {cartLineImages}
            {viewState.isShowAddress && <AddressShow {...showAddress} />}
            {viewState.isShowAddresList && <AddressSelect {...showAddressSelect} />}
            {viewState.isShowAddOrUpdateAddress && <AddressAddUpdate {...showAddOrUpdateAddress} />}
            {deliveryNotes}
        </Module>
    );
};

export default CheckoutShippingAddressView;

5. Copy action and common folders from D365.Commerce.Modules

After cloning the checkout billing address module then build module if it shows build error for can not find namce space microsoft then need to add dependency file, Copy action and common folders from D365.Commerce.Modules Copy action and common folders from packages\address\src in module repo and place in sample repo in src folder to resolve dependencies. Remove tests folder from src/action and src/common folders.

6. Add Style changes

In this step, we will add the delivery notes style. In the file with name checkout-shipping-address.scss under src/themes/fabrikam-extended/styles/04-modules add the below code in it.

.delivery-notes {
        padding-top: 20px;
        clear: both;
        .deliveryNotes-textarea {
          width: 100%;
          min-height: 150px;
          padding: 5px;
          font-size: var(--msv-address-font-size);
          color: var(--msv-checkout-shipping-address-font-color);
          border: none;
          &:focus {
            outline: none;
          }
        }
      }

7. HQ setting for creating attribute.

Follow the instructions mentioned in document to set up the attribute DELIVERYNOTES in HQ side and mapping with Channel.

Overview

8. HQ Extensions.

The “DELIVERYNOTES” will be included as part of the cart line which on the sales order form in header details by using the chain of Commands (COC) and requires a headquarters extension. The extension is provided within this sample and will need to be deployed Document.

Overview

Please see instructions on how to import the x++ project. document

9. Package and Deployment for RTS extension.

Please refer below documentation on creating package and deploy the extension: https://docs.microsoft.com/en-us/dynamics365/fin-ops-core/dev-itpro/deployment/create-apply-deployable-package

10. Validate the HQ UI Changes

When order created from eCom, need to store the Delivery Notes on Sales order Header level. DeliveryNotes should be saved on the SalesOrder Form >> SalesLine Details Account Receivable >> All Sales Orders >> double click on the selected order.

Overview

Build and test module

The sample can now be tested in a web browser using the yarn start command.

1.Test by using mock file

Please refer mock file under the src/pageMocks/checkout.json, pageMock.

Test the module using page mock, Go to browser and copy paste the below url to verify delivery notes in checkout page.https://localhost:4000/page?mock=checkout&theme=fabrikam-extended.

Note: In the checkout mock, item can be wiped so always take latest mock for guest or auth user, for guest user first navigate to https://localhost:4000/modern If item is not in the checkout then add item into cart if item not added from local host then it will always send to cart page with back to shopping button. Take mock again and change the module name from checkout-shipping-address to custom-checkout-shipping-address in the taken mock.

2. Test Integration test case

  • Integration test case for sample can be tested in browser by setting the path to DeliveryNoteAttribute sample level in command propmt and run yarn testcafe chrome .\test\checkout-render-tests.ts -s .\ command.

  • Ensure that testcafe is added globally to run test case.

Third party Image and Video Usage restrictions

The software may include third party images and videos that are for personal use only and may not be copied except as provided by Microsoft within the demo websites. You may install and use an unlimited number of copies of the demo websites., You may not publish, rent, lease, lend, or redistribute any images or videos without authorization from the rights holder, except and only to the extent that the applicable copyright law expressly permits doing so.