Technical refinement - Front end

Technical refinement - Front end

Repos that we’ll change:

Whitelabel: https://github.com/rbilabs/intl-whitelabel-app

 

Figma: @Schroer, Gabriel (Deactivated)


Task summary - Whitelabel:


Components tree architecture

Schema to show how the payment method options will behavior in the application

Business rules

  • The new payment method should only be available if the user selects “delivery mode” inside the Whitelabel App

  • The confirmation/tracking page should show the correct text in the payment reminder for this new payment method

  • The receipt email should show the correct payment option for this new payment method

  • The new method should be available in the user account methods list


Tasks breakdown

The solution was based on the Sodexo Cheque Gourmet Voucher implementation: https://github.com/rbilabs/intl-whitelabel-app/pull/686/files

Task 1: Create a new feature flag

Flag should be added in: frontend/src/utils/launchdarkly/flags.ts

Suggestion name: enable-payment-on-delivery-card

  • The user can pay using credit or debit mode, so “card” is a generic name

https://app.launchdarkly.com/intl-guest-app/dev-plk-es/features/enable-payment-on-delivery-card-paycomet/targeting

 

Task 2: Add the new payment method in the payment state and structure

  • Create a new placeholder for the new payment method

    • The idea is to send this new payment method identified as cash

// Replace this name for what we decide export const NEW_PAYMENT_METHOD_PLACEHOLDER: IPaymentMethod = { sodexo: false, fdAccountId: 'CASH', accountIdentifier: 'NEW_PAYMENT_METHOD', // TO BE DEFINED paymentMethodBrand: 'NEW_PAYMENT_METHOD', // TO BE DEFINED chaseProfileId: null, credit: null, prepaid: null, paypalIdentifier: null, ideal: null, paypal: false, };
  • Add the new payment method in getPaymentMethodsState:

Update the interface:

frontend/src/state/payment/hooks/types.ts:

export interface ISetupPaymentMethodState { enableNewMethod: boolean; }

 

Adjust the getPaymentMethodsState:

export const getPaymentMethodsState = ({ // rest of the code enableNewMethod, }: ISetupPaymentMethodState) => { // rest of the code if (enableNewMethod) { availablePaymentMethodList.push(NEW_PAYMENT_METHOD_PLACEHOLDER); } // rest of the code };

 

  • Adjust the payment hook

Update cash account variable:

const cashAccount: IPaymentMethod = { // rest of the code newMethod: null, };

 

Add the new feature flag here and then pass through the getPaymentMethodState:

const usePayment = ({ // rest of the code const enableNewMethod = useFlag(LaunchDarklyFlag.ENABLE_CREDIT_CARD_AT_HOME_PAYCOMET); // new line const initPaymentMethods = useCallback(() => { const { availablePaymentMethodList, validDefaultPaymentMethodId, validDefaultReloadPaymentMethodId, } = getPaymentMethodsState({ enableNewMethod, // new line here }); }, [] ); });

 

Adjust the unit tests mocking the new flag where necessary: frontend/src/state/payment/tests/use-payment.test.tsx

  • Adjust use-order-payment hook

Add a new state for the new payment method:

const [isPaycometNewMethodSelected, setIsPaycometNewMethodSelected] = useState('');

 

Update the handlePaymentMethodSelected adding the new method:

const handlePaymentMethodSelected = (newFdAccountId?: string) => { if (!newFdAccountId) { payment.setCheckoutPaymentMethodId(''); return; } // rest of the code setIsPaycometNewMethodSelected(''); // New line };

 

Clear our new method in the useEffect logic that already exist:

// when checkout payment method have been updated from payment context // the flags to show add new credit card or add gift card // have to be updated. useEffect(() => { // rest of the code setIsPaycometNewMethodSelected(''); } }, [setCheckoutPaymentMethodId, checkoutPaymentMethodId]);

 

Update the placeOrder function to deal with the new method:

  • Option 1:

    • Adjust the if condition for paycomet processor to deal with the new method:

      } else if (payment.isPaycomet) { // rest of the code if (payCometValues.isPaypal) { // rest of the code } else { // add a new condition here for our new method, like the sodexo one let paymentMethodBrandValue = undefined; if (isPaycometSodexoMethodSelected || isPaycometChequeGourmetMethodSelected) { paymentMethodBrandValue = isPaycometSodexoMethodSelected ? PaymentMethodBrand.SODEXO : PaymentMethodBrand.CHEQUE_GOURMET; } commitInput = { // rest of the code }; } // rest of the code

       

  • Option 2:

    • Separate this logic in another reusable block and then pass the new paymentMethodBrand (as a kind of builder pattern or something similar). We can discuss that on an A&D section.

 

Adjust the processOrderWithAccount function for the new method like this Sodexo example:

// rest of the code const isPaymentWithVoucher = paymentAccount.paymentMethodBrand === PaymentMethodBrand.SODEXO_VOUCHER || paymentAccount.paymentMethodBrand === PaymentMethodBrand.CHEQUE_GOURMET_VOUCHER; if (payment.checkoutPaymentMethodId === CASH_ACCOUNT_IDENTIFIER || isPaymentWithVoucher) { const paymentDetails: IPayment = { cashPayment: true, fullName, paymentMethodBrand: paymentAccount?.paymentMethodBrand ?? undefined, }; if (payment.isVrPayment) { // The GraphQL endpoint for VR Payment expects this object even // for Cash orders, otherwise the order creation fails paymentDetails.vrPaymentInput = { merchantAccount: '', pspReference: '', storePaymentMethod: true, }; } return commitOrder({ ...commitInput, creditType: CASH_ACCOUNT_IDENTIFIER, payment: paymentDetails, }); }

 

Adjust the handleOrderCommit to place the order with the new method:

// Add new condition here as the isPaymentWithVoucher from the Sodexo example const isPaymentWithVoucher = isPaymentWithSodexoVoucher || isPaymentWithChequeGourmetVoucher; if ( (showAddPaymentMethod || isAdyenBlikMethodSelected || reloadAddPaymentMethod || isPayPalRegistration) && !(selectedPaymentMethod?.cash || isPaymentWithVoucher || payment.isFreeOrderPayment) ) { await placeOrder(); } else { await placeOrderWithAccount(); }
  • Extend the getCurrentSelectedMethodType:

Add the new payment in the enum CurrentSelectedMethodType:

  • frontend/src/components/payment-method/types.ts:

export enum CurrentSelectedMethodType { // rest of the code ADD_NEW_PAYMENT_NEW_METHOD = 'addNewPaymentNewMethod', }

 

Adjust the IPaymentMethodProps:

export interface IPaymentMethodProps { // rest of the code setIsPaycometNewMethodSelected?: Dispatch<SetStateAction<string>>; isPaycometNewMethodSelected?: string; }

 

Add the new condition in getCurrentSelectedMethodType:

type GetCurrentSelectedMethodTypeParams = { // rest of the code isPaycometNewMethodSelected?: string; }; function getCurrentSelectedMethodType({ // rest of the code isPaycometNewMethodSelected }: GetCurrentSelectedMethodTypeParams): CurrentSelectedMethodType { let currentSelectedMethodType = CurrentSelectedMethodType.ADD_NEW_PAYMENT_METHOD; // rest of the code if (isPaycometNewMethodSelected === 'card') { currentSelectedMethodType = CurrentSelectedMethodType.ADD_NEW_PAYMENT_NEW_METHOD; } // rest of the code }

 

  • Adjust order-payment comp:

Return the new create state from useOrderPayment:

const { // rest of the code isPaycometNewMethodSelected, // new line here } = useOrderPayment({ serverOrder, onPriceOrder, enableOrderTimedFire, });

 

Pass the returned value to the getCurrentSelectedMethodType:

const currentSelectedMethodType = getCurrentSelectedMethodType({ isPaycometNewMethodSelected, });

 

Pass the new method to the <PaymentMethod> comp:

<PaymentMethod // rest of the code isPaycometNewMethodSelected={isPaycometNewMethodSelected} />

 

Task 3: Add the new method in payment-method structure

  • Create new styled component for the new payment method

// ... rest of the file export const StyledNewMethodCardIcon = styled(IconNewMethod)` ${cardIconSize} `;
  • Add a new type

export type PaymentMethodOptionProps = { // ... rest of the file setIsPaycometNewMethodSelected?: Dispatch<SetStateAction<string>>; };
  • Add the new method in payment-method comp

Add new props in PaymentMethod:

function PaymentMethod({ // rest of the code isPaycometNewMethodSelected, setIsPaycometNewMethodSelected, }: IPaymentMethodProps) { // rest of the code // pass the new prop in the getCurrentSelectedMethodType const currentSelectedMethodType = getCurrentSelectedMethodType({ isPaycometNewMethodSelected }); });

 

Create new item inside PaymentMethod comp:

// ... rest of the file const SelectedNewMethodOptionItem = ( <PaymentMethodAddNewItemOption data-testid="payment-new-method-name" text={formatMessage({ id: 'addNewPaymentNewMethod' })} Icon={<StyledNewMethodCardIcon />} onClick={getSelectedMethodClickHandler()} selected /> );

 

Add the new method in the map structure:

const CurrentSelectedMethodMap = { // ... rest of the file [CurrentSelectedMethodType.ADD_NEW_PAYMENT_METHOD]: SelectedNewMethodOptionItem, };

 

Pass the new prop in the <PaymentMethodOptions> comp:

<PaymentMethodOptions setIsPaycometNewMethodSelected={setIsPaycometNewMethodSelected} />
  • Add the new method in payment-method-options comp

Add the new prop in PaymentMethodOptions comp:

function PaymentMethodOptions({ // .. rest of the file setIsPaycometNewMethodSelected }: PaymentMethodOptionProps) { const enableNewMethod = useFlag(LaunchDarklyFlag.ENABLE_CREDIT_CARD_AT_HOME_PAYCOMET); // new line });

 

Add new condition in handleUnselectedMethodClick, handleAddNewCardOptionClick, handleAddNewGiftCardOptionClick, handlePaypalOptionClick and handleAdyenIdealOptionClick to clear our new state:

if (setIsPaycometSodexoOptionSelected) { setIsPaycometNewMethodSelected(''); }

 

Add a new handle for our new method (using useCallback) where we’ll clear the other methods and set your:

const handleNewMethodOptionClick = useCallback( (e: React.FormEvent<HTMLButtonElement>, newMethodType: string) => { e.preventDefault(); setIsAddNewCardOptionSelected(false); setIsAddNewGiftCardOptionSelected(false); if (setIsPaycometPaypalOptionSelected) { setIsPaycometPaypalOptionSelected(false); } if (setIsAdyenIdealOptionSelected) { setIsAdyenIdealOptionSelected(false); } if (setIsAdyenBlikOptionSelected) { setIsAdyenBlikOptionSelected(false); } if (setIsPaycometSodexoOptionSelected) { setIsPaycometSodexoOptionSelected(''); } if (setIsPaycometSodexoOptionSelected) { setIsPaycometSodexoOptionSelected(''); } // our new method here if (setIsPaycometNewMethodSelected) { setIsPaycometNewMethodSelected(newMethodType); if (newMethodType === 'card') { setIsAddNewCardOptionSelected(true); } } handleSelect(); setCollapsed(true); }, [ // callback dependencies here ] );

 

Add in the enableSortPaymentMethods && unSelectedMethods.map a new block for our new method:

  • We also need to condition this payment method to be shown only if delivery mode is selected

const isDelivery = serverOrder?.delivery; {method?.newMethod && method?.transient && isDelivery && ( <StyledOption> <PaymentMethodOptionItemDivider /> <PaymentMethodAddNewItemOption data-testid="add-new-method-payment" text={formatMessage({ id: newMethodLabel(isGuestOrder) })} Icon={<StyledNewMethodCardIcon />} onClick={e => handleNewMethodOptionClick(e, 'card')} isLoading={isLoadingPopup} /> </StyledOption> )}

 

Add a new block for for the case where we not have the enableSortPaymentMethods as true:

{!enableSortPaymentMethods && enableNewMethod && ( <StyledOption> <PaymentMethodOptionItemDivider /> <PaymentMethodAddNewItemOption data-testid=add-new-method-payment" text={formatMessage({ id: newMethodLabel(isGuestOrder) })} Icon={<StyledNewMethodCardIcon />} onClick={e => handleNewMethodOptionClick(e, 'card')} isLoading={isLoadingPopup} /> </StyledOption> )}

 

Add the new payment method in the sortPaymentMethods

  • frontend/src/components/payment-method/utils/sort-payment-methods.ts

const paymentMethodListTypeOrder = [ // rest of the code 'NEW_METHOD', ]; // Add new case in switch for our new method function filterMethodByType(typeOrder: string, paymentMethods: IPaymentMethod[]): IPaymentMethod[] { switch (typeOrder) { // rest of the code case 'NEW_METHOD': return paymentMethods.filter( p => !p?.transient && p?.newMethod && p.accountIdentifier === typeOrder ); // rest of the code } }

 

Add a new logic to the filter !enableSortPaymentMethods && unSelectedMethods.filter(isNotLocalWallet)... to now show our payment method if delivery mode is not selected:

const isDelivery = serverOrder?.delivery; const isNotLocalWallet = (method: IPaymentMethod) => { return !method.blik && !method.ideal; }; const sortedMethodsFilterConditions = (method: IPaymentMethod) => { if (method.newMethod && !isDelivery) { return false; // remove our item from the list if delivery is not selected } return isNotLocalWallet(method); }; // A suggestion would be the creation of a new function for the filter conditions {!enableSortPaymentMethods && unSelectedMethods.filter(sortedMethodsFilterConditions).map(method => {

 

  • Add the new method in paycomet-hosted-page-payment comp

Return the new values from the useOrderPayment hook

const { // rest of code setIsPaycometNewMethodSelected, isPaycometNewMethodSelected, } = useOrderPayment({ enableOrderTimedFire, onPriceOrder, serverOrder, });

 

Pass down the values to the <PaymentMethod> comp

// rest of the code <PaymentInfoBackground $hostedPayment> {showPaymentMethodSelection && ( <PaymentMethod // rest of the code setIsPaycometNewMethodSelected={setIsPaycometNewMethodSelected} // new line isPaycometNewMethodSelected={isPaycometNewMethodSelected} // new line onResult={onPaymentOutcome} /> )}

 

Task 4: Create and add a new method in payment-method-option structure

Just an example of where the new change will be

 

  • Add the new icon for the new payment method in: frontend/src/components/icons/new-payment-method/index.tsx (replace “new-payment-method” for the new name, hahaha).

    • We can use the SVGR Playground - SVGR to convert the svg code from Figma to use in our code (following the pattern of the application)

  • Add the new method option in the interface of payment methods:

export interface IPaymentMethod { // rest of the code newPaymentMethod?: boolean | null; }

 

  • Add new file in frontend/src/components/payment-method-option/as the following example:

    • Remember to add the translation that will be used in formatMessage

import React from 'react'; import { useIntl } from 'react-intl'; import { MethodType } from './styled'; const NewPaymentMethod = () => { // Example SodexoMethod const { formatMessage } = useIntl(); return <MethodType>{formatMessage({ id: 'payWithNewMethod' })}</MethodType>; }; export default NewPaymentMethod;
  • Use the created payment method option in payment-method-option:

Adjust the RenderMethodType and add the new method created above

const RenderMethodType = () => { // rest of the code // New if here if (method.newPaymentMethod) { return <NewPaymentMethod />; } return null; }; // add the new payment method inside the <MethodTypeWrapper> in the return return // rest of the code ( // rest of the code <MethodTypeWrapper data-private data-dd-privacy="mask" onClick={onClickMethod} $isClickable={!!onClick} disableMethod={disableMethod} fromCheckout={fromCheckout} data-testid={`method-type-wrapper-${method.accountIdentifier ?? method.fdAccountId ?? ''}`} selected={selected} > // rest of the code <div> <MethodTypeDescription> // Add new method here inside the description {method.newPaymentMethod && <StyledNewPaymentMethodCardIcon />} </MethodTypeDescription> // rest of the code </div> </MethodTypeWrapper> // rest of the code );

 

 

Task 5: Adjust account payment method lists to deal with the new method

Adjusting the list to deal with the new method

We’ll exclude the new payment method if it is not actually stored in the user account:

function filterOutUnsupportedPaymentMethods(paymentMethods: IPaymentMethod[]) { return paymentMethods.filter( n => n.accountIdentifier !== PAYPAL_PAYMENT_METHOD_PLACEHOLDER.accountIdentifier && n.accountIdentifier !== SODEXO_VOUCHER_PAYMENT_METHOD_PLACEHOLDER.accountIdentifier && n.accountIdentifier !== NEW_PAYMENT_METHOD_PLACEHOLDER.accountIdentifier && ); }

 

Nice to have - not considered now

Task 6: Adjust the confirmation text for confirmation page and tracking modal

Confirmation page
Tracking info modal
  • The same component is used to show both cases: frontend/src/pages/order-confirmation/delivery/delivery-details.tsx

Suggestion of implementation:

  • Add a new property/flag inside the cart.payment structure to enable a new condition to show new texts for these case (I think that perhaps we should not change the cardType, not sure yet)

For the payment reminder tip we have some options:

  1. Create new component as the <CashPaymentReminder /> that we have today for the new condition/new text

  2. Adjust the <CashPaymentReminder /> to deal with other types of texts and perhaps rename this component to have a generic name (for example <PaymentReminder />)

 

Task 7: Adjust the receipt email to show the correct message for the new payment method

Today, when the commit order is processed, order-service triggers Braze API with the order info, that then sends the confirmation email to the customer.

The first necessary alteration will be to include paymentMethodBrand in the properties sent to braze in transformOrderProperties utility method. For example:

 

export const transformOrderProperties = ({ rbiOrder, brand, fzStore, language, }: ITransformOrderProperties): Record<string, any> => { ... const { cardType, ccLast4: last4, paymentType, paymentMethodBrand }: Partial<IPayment> = payment || {}; ... properties.paymentMethodBrand = paymentMethodBrand; return properties; }

After we change intl-packages/email to send the necessary information to Braze API, we need to update the templates configured, which are split into templates and content blocks (reusable pieces of content). Using PLK-ES as example we can see:

The example displays a LiquidJS template, which is similar but not equal to Braze templates. Braze access properties using the syntax {{event_properties.${language}}}

 

{% assign people = "alice" | split: ", " -%} {% assign cardType = "CASH" %} {% assign paymentMethodBrand = "PAYMENT_ON_DELIVERY_CARD" %} {% assign last4 = "1234" %} <ul> {%- for person in people %} <li> <a href="{{person | prepend: "http://example.com/"}}"> {{ person | capitalize }} </a> {{cardType}} Pagado con {% if cardType != "CASH" %} Tarjeta terminada en {{last4}} {% else %}{% if paymentMethodBrand != nil %}{{paymentMethodBrand}} {%else%}efectivo {%endif%} {%endif%} </li> {%- endfor%} </ul>