Repos that we’ll change:
Whitelabel App: https://github.com/rbilabs/intl-whitelabel-app
Figma: Schroer, Gabriel (Deactivated)
Sequence Diagram
Business rules
A new Bizum payment must be created for Paycomet.
This payment must work the same as Paycomet's PayPal, it must generate a link and be used in an Iframe where the payment will be processed.
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
We have to pay attention to changes from the Payments team, they are making some changes to their routine, and this new implementation cannot change the current behavior.
There are two flags that change the application's behavior: enable-paycomet-webhook-notifications (BE) and enable-backend-commit-order (BE & FE), we must test both ways, with the flags enabled and disabled.
Tasks breakdown
Task 1: Create a new feature flag
Flag should be added in: src/utils/launchdarkly/flags.ts
Suggestion name: enable-bizum-paycomet
This flag is used to show Bizum payment in payment lists.
Example of use: frontend/src/state/payment/hooks/use-payment.tsx
Task 2: Add the new payment method in the payment state and structure
frontend/src/state/payment/types.ts
export interface IPaymentMethod {
accountIdentifier: string | null;
fdAccountId: string | null;
paymentMethodBrand?: string | null;
chaseProfileId: string | null;
paypalIdentifier: string | null;
credit: ICredit | null;
selected?: boolean;
prepaid: IPrepaid | null;
// make cash required eventually
cash?: boolean | null;
paypal: boolean | null;
ideal: boolean | null;
blik?: boolean | null;
sodexo?: boolean | null;
chequeGourmet?: boolean | null;
paymentOnDeliveryCard?: boolean | null;
bizum?: boolean | null;
transient?: boolean;
}
frontend/src/state/payment/constants.ts
export const BIZUM_PAYMENT_METHOD_PLACEHOLDER: IPaymentMethod = {
bizum: true,
fdAccountId: 'BIZUM',
accountIdentifier: 'BIZUM',
paymentMethodBrand: 'BIZUM',
chaseProfileId: null,
credit: null,
prepaid: null,
paypalIdentifier: null,
ideal: null,
paypal: false,
sodexo: null,
chequeGourmet: false,
transient: true,
};
frontend/src/state/payment/hooks/getPaymentMethodsState/index.ts
Update the interface:
frontend/src/state/payment/hooks/types.ts
:
export interface ISetupPaymentMethodState {
enableBizum: boolean;
}
Adjust the getPaymentMethodsState
:
export const getPaymentMethodsState = ({
// rest of the code
enableNewMethod,
}: ISetupPaymentMethodState) => {
// rest of the code
if (enableBizum) {
availablePaymentMethodList.push(BIZUM_PAYMENT_METHOD_PLACEHOLDER);
}
// rest of the code
};
frontend/src/state/payment/hooks/use-payment.tsx
Add the new feature flag here and then pass through the getPaymentMethodState
:
const usePayment = ({
// rest of the code
const enableBizum = useFlag(LaunchDarklyFlag.ENABLE_BIZUM_PAYCOMET); // new line
const initPaymentMethods = useCallback(() => {
const {
availablePaymentMethodList,
validDefaultPaymentMethodId,
validDefaultReloadPaymentMethodId,
} = getPaymentMethodsState({
enableBizum, // new line here
});
},
[]
);
});
Adjust the unit tests mocking the new flag where necessary: frontend/src/state/payment/tests/use-payment.test.tsx
workspaces/frontend/src/utils/payment/index.ts
Update isPayPay to isPayLink and make a refactor code.
export interface IPaycometState extends IPaymentState {
merchantAccount?: string;
pspReference?: string;
jetId?: string;
iframeUrl?: string;
isPayLink?: boolean;
card?: {
expiryMonth: string;
expiryYear: string;
bin: string;
last4: string;
};
paymentMethodBrand?: PaymentMethodBrand;
paytpvToken?: string;
}
frontend/src/pages/cart/payment/order-payment/use-order-payment.ts
Add a new state for the new payment method:
const [isPaycometBizumMethodSelected, setIsPaycometBizumMethodSelected] = useState(false);
Update the handlePaymentMethodSelected
adding the new method:
const handlePaymentMethodSelected = (newFdAccountId?: string) => {
if (!newFdAccountId) {
payment.setCheckoutPaymentMethodId('');
return;
}
// rest of the code
setIsPaycometBizumMethodSelected(false); // 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
setIsPaycometBizumMethodSelected(false);
}
}, [setCheckoutPaymentMethodId, checkoutPaymentMethodId]);
Update the placeOrder
function to deal with the new method:
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();
}
frontend/src/components/payment-method/utils/get-current-selected-method-type.ts
Add the new payment in the enum CurrentSelectedMethodType
:
export enum CurrentSelectedMethodType {
// rest of the code
ADD_NEW_PAYMENT_BIZUM = 'addNewPaymentBizum',
}
Adjust the IPaymentMethodProps
:
export interface IPaymentMethodProps {
// rest of the code
setIsPaycometBizumMethodSelected?: Dispatch<SetStateAction<boolean>>;
isPaycometBizumMethodSelected?: boolean;
}
Add the new condition in getCurrentSelectedMethodType
:
type GetCurrentSelectedMethodTypeParams = {
// rest of the code
isPaycometBizumMethodSelected?: boolean;
};
function getCurrentSelectedMethodType({
// rest of the code
isPaycometNewMethodSelected
}: GetCurrentSelectedMethodTypeParams): CurrentSelectedMethodType {
let currentSelectedMethodType = CurrentSelectedMethodType.ADD_NEW_PAYMENT_METHOD;
// rest of the code
if (isPaycometBizumMethodSelected) {
currentSelectedMethodType = CurrentSelectedMethodType.ADD_NEW_PAYMENT_BIZUM;
}
// rest of the code
}
frontend/src/pages/cart/payment/order-payment/payment.tsx
Return the new create state from useOrderPayment
:
const {
// rest of the code
isPaycometBizumMethodSelected, // new line here
} = useOrderPayment({
serverOrder,
onPriceOrder,
enableOrderTimedFire,
});
Pass the returned value to the getCurrentSelectedMethodType
:
const currentSelectedMethodType = getCurrentSelectedMethodType({
isPaycometBizumMethodSelected,
});
Pass the new method to the <PaymentMethod>
comp:
<PaymentMethod
// rest of the code
isPaycometBizumMethodSelected={isPaycometBizumMethodSelected}
/>
Task 3: Add the new method in payment-method structure
frontend/src/components/payment-method/styled.ts
// ... rest of the file
export const StyledBizumMethodCardIcon = styled(IconNewMethod)`
${cardIconSize}
`;
frontend/src/components/payment-method/payment-method-options/types.ts
export type PaymentMethodOptionProps = {
// ... rest of the file
setIsPaycometBizumMethodSelected?: Dispatch<SetStateAction<boolean>>;
};
workspaces/frontend/src/components/payment-method/index.tsx
Add new props in PaymentMethod
:
function PaymentMethod({
// rest of the code
isPaycometBizumMethodSelected,
setIsPaycometBizumMethodSelected,
}: IPaymentMethodProps) {
// rest of the code
// pass the new prop in the getCurrentSelectedMethodType
const currentSelectedMethodType = getCurrentSelectedMethodType({
isPaycometBizumMethodSelected
});
});
Create new item inside PaymentMethod
comp:
// ... rest of the file
const SelectedNewMethodOptionItem = (
<PaymentMethodAddNewItemOption
data-testid="payment-bizum-method-name"
text={formatMessage({ id: 'addNewPaymentBizumMethod' })}
Icon={<StyledNewMethodCardIcon />}
onClick={getSelectedMethodClickHandler()}
selected
/>
);
Add the new method in the map structure:
const CurrentSelectedMethodMap = {
// ... rest of the file
[CurrentSelectedMethodType.ADD_NEW_PAYMENT_BIZUM]: SelectedBizumOptionItem,
};
Pass the new prop in the <PaymentMethodOptions>
comp:
<PaymentMethodOptions
setIsPaycometBizumMethodSelected={setIsPaycometBizumMethodSelected}
/>
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 enableBizumMethod = useFlag(LaunchDarklyFlag.ENABLE_BIZUM_PAYCOMET); // new line
});
Add new condition in handleUnselectedMethodClick
, handleAddNewCardOptionClick
, handleAddNewGiftCardOptionClick
, handlePaypalOptionClick
and handleAdyenIdealOptionClick
to clear our new state:
if (setIsPaycometSodexoOptionSelected) {
setIsPaycometBizumMethodSelected(false);
}
Add a new handle for our new method (using useCallback) where we’ll clear the other methods and set your:
const handleBizumOptionClick = useCallback(
(e: React.FormEvent<HTMLButtonElement>) => {
e.preventDefault();
setIsAddNewCardOptionSelected(false);
setIsAddNewGiftCardOptionSelected(false);
if (setIsPaycometBizumOptionSelected) {
setIsPaycometBizumOptionSelected(true);
}
if (setIsPaycometPaypalOptionSelected) {
setIsPaycometPaypalOptionSelected(false);
}
if (setIsAdyenIdealOptionSelected) {
setIsAdyenIdealOptionSelected(false);
}
if (setIsAdyenBlikOptionSelected) {
setIsAdyenBlikOptionSelected(false);
}
if (setIsPaycometSodexoOptionSelected) {
setIsPaycometSodexoOptionSelected('');
}
if (setIsPaycometChequeGourmetOptionSelected) {
setIsPaycometChequeGourmetOptionSelected('');
}
openPaycometPayLinklPopup('BIZUM');
},
[
setIsAddNewCardOptionSelected,
setIsAddNewGiftCardOptionSelected,
setIsPaycometBizumOptionSelected,
setIsPaycometPaypalOptionSelected,
setIsAdyenIdealOptionSelected,
setIsAdyenBlikOptionSelected,
setIsPaycometSodexoOptionSelected,
setIsPaycometChequeGourmetOptionSelected,
openPaycometPayLinklPopup,
]
);
Add in the enableSortPaymentMethods && unSelectedMethods.map
a new block for our new method:
{method?.bizum && method?.transient && (
<StyledOption>
<PaymentMethodOptionItemDivider />
<PaymentMethodAddNewItemOption
data-testid="add-bizum-payment"
text={'Bizum'}
Icon={<StyledBizumCardIcon />} // should create this icon
onClick={handleBizumOptionClick}
isLoading={isLoadingPopup}
/>
</StyledOption>
)}
Add a new block for for the case where we not have the enableSortPaymentMethods
as true:
{!enableSortPaymentMethods && isBizumEnabled && (
<StyledOption>
<PaymentMethodOptionItemDivider />
<PaymentMethodAddNewItemOption
data-testid="add-bizum-payment"
text={formatMessage({ id: bizumlLabel(isGuestOrder) })}
Icon={<StyledPaypalCardIcon />}
onClick={handlePaypalOptionClick}
isLoading={isLoadingPopup}
/>
</StyledOption>
)}
Add the new payment method in the sortPaymentMethods
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 'BIZUM':
return paymentMethods.filter(
p => !p?.transient && p?.bizum && 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 isNotLocalWallet = (method: IPaymentMethod) => {
return !method.blik && !method.ideal;
};
const sortedMethodsFilterConditions = (method: IPaymentMethod) => {
if (method.bizum) {
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 => {
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
setIsPaycometBizumMethodSelected,
isPaycometBizumMethodSelected,
} = useOrderPayment({
enableOrderTimedFire,
onPriceOrder,
serverOrder,
});
Pass down the values to the <PaymentMethod> comp
// rest of the code
<PaymentInfoBackground $hostedPayment>
{showPaymentMethodSelection && (
<PaymentMethod
// rest of the code
setIsPaycometBizumMethodSelected={setIsPaycometBizumMethodSelected} // new line
isPaycometBizumMethodSelected={isPaycometBizumMethodSelected} // 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
bizumPaymentMethod?: boolean | null;
}
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: 'payWithBizumMethod' })}</MethodType>;
};
export default NewPaymentMethod;
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.bizumPaymentMethod) {
return <BizumPaymentMethod />;
}
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.bizumPaymentMethod && <StyledBizumPaymentMethodCardIcon />}
</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 !== BIZUM_PAYMENT_METHOD_PLACEHOLDER.accountIdentifier &&
);
}
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.
We can create a validation when cardType different CASH and ccLast4 is empty.
packages/emails/src/utils.ts
export const transformOrderProperties = ({
rbiOrder,
brand,
fzStore,
language,
}: ITransformOrderProperties): Record<string, any> => {
...
const { cardType, ccLast4, paymentType, paymentMethodBrand }: Partial<IPayment> = payment || {};
let last4 = ccLast4;
if (cardType !== RBIPaymentCard.CASH && !last4?.trim().length) {
last4 = '****';
}
...
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:
Add Comment