Skip to end of metadata
Go to start of metadata

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 12 Next »

Repos that we’ll change:

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

Figma: Schroer, Gabriel (Deactivated)


Task summary - Whitelabel:


Components tree architecture


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_CARD_AT_HOME_PAYCOMET

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

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

 frontend/src/state/payment/constants.ts
// 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:

 frontend/src/state/payment/hooks/getPaymentMethodsState/index.ts

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

 frontend/src/state/payment/hooks/use-payment.tsx

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

 frontend/src/pages/cart/payment/order-payment/use-order-payment.ts

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:

 frontend/src/components/payment-method/utils/get-current-selected-method-type.ts

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:

 frontend/src/pages/cart/payment/order-payment/payment.tsx

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

 frontend/src/components/payment-method/styled.ts
// ... rest of the file
export const StyledNewMethodCardIcon = styled(IconNewMethod)`
  ${cardIconSize}
`;
  • Add a new type

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

 workspaces/frontend/src/components/payment-method/index.tsx

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

 frontend/src/components/payment-method/payment-method-options/payment-method-options.tsx

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.chequeGourmet && !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

 frontend/src/pages/cart/payment/order-payment/paycomet-hosted-page-payment/paycomet-hosted-page-payment.tsx

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

  • 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).

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

 frontend/src/state/payment/types.ts
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

 frontend/src/components/payment-method-option/new-payment-method.tsx (example of new file)
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:

 frontend/src/components/payment-method-option/index.tsx

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

 frontend/src/components/payment-method-flat-list-with-button/payment-method-flat-list-with-button.tsx

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 &&
  );
}

  • No labels