Repos that we’ll change:
Whitelabel: https://github.com/rbilabs/intl-whitelabel-app
Figma: Schroer, Gabriel (Deactivated)
Task summary - Whitelabel:
Table of Contents |
---|
minLevel | 2 |
---|
maxLevel | 6 |
---|
include | |
---|
outline | false |
---|
indent | |
---|
exclude | |
---|
type | list |
---|
class | |
---|
printable | false |
---|
|
Components tree architecture
Image Modified
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
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
Expand |
---|
title | frontend/src/state/payment/constants.ts |
---|
|
Code Block |
---|
| // 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,
}; |
|
Expand |
---|
title | frontend/src/state/payment/hooks/getPaymentMethodsState/index.ts |
---|
|
Update the interface: frontend/src/state/payment/hooks/types.ts :
Code Block |
---|
| export interface ISetupPaymentMethodState {
enableNewMethod: boolean;
} |
Adjust the getPaymentMethodsState : Code Block |
---|
| export const getPaymentMethodsState = ({
// |
|
...
rest of the code
enableNewMethod,
}: ISetupPaymentMethodState) => {
// |
|
...
rest of the code
if (enableNewMethod) {
availablePaymentMethodList.push(NEW_PAYMENT_METHOD_PLACEHOLDER);
}
// |
|
...
Expand |
---|
title | frontend/src/state/payment/hooks/use-payment.tsx |
---|
|
Update cash account variable: Code Block |
---|
| const cashAccount: IPaymentMethod = {
// |
|
...
rest of the code
newMethod: null,
}; |
Add the new feature flag here and then pass through the getPaymentMethodState : Code Block |
---|
| 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 |
Expand |
---|
title | 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: Code Block |
---|
| 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: Code Block |
---|
| // 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(() => {
// |
|
...
...
of the code
setIsPaycometNewMethodSelected('');
}
}, [setCheckoutPaymentMethodId, checkoutPaymentMethodId]); |
Update the placeOrder function to deal with the new method: |
...
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 = {
// |
|
...
...
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: Code Block |
---|
| // 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();
} |
|
Expand |
---|
title | frontend/src/components/payment-method/utils/get-current-selected-method-type.ts |
---|
|
Add the new payment in the enum CurrentSelectedMethodType : Code Block |
---|
| export enum CurrentSelectedMethodType {
// |
|
...
rest of the code
ADD_NEW_PAYMENT_NEW_METHOD = 'addNewPaymentNewMethod',
} |
Adjust the IPaymentMethodProps : Code Block |
---|
| export interface IPaymentMethodProps {
// |
|
...
rest of the code
setIsPaycometNewMethodSelected?: Dispatch<SetStateAction<string>>;
isPaycometNewMethodSelected?: string;
} |
Add the new condition in getCurrentSelectedMethodType : Code Block |
---|
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;
}
// |
|
...
Expand |
---|
title | frontend/src/pages/cart/payment/order-payment/payment.tsx |
---|
|
Return the new create state from useOrderPayment : |
...
rest of the code
isPaycometNewMethodSelected, // new line here
} = useOrderPayment({
serverOrder,
onPriceOrder,
enableOrderTimedFire,
}); |
Pass the returned value to the getCurrentSelectedMethodType : Code Block |
---|
| const currentSelectedMethodType = getCurrentSelectedMethodType({
isPaycometNewMethodSelected,
}); |
Pass the new method to the <PaymentMethod> comp: Code Block |
---|
| <PaymentMethod
// |
|
...
rest of the code
isPaycometNewMethodSelected={isPaycometNewMethodSelected}
/> |
|
Task 3: Add the new method in payment-method structure
Expand |
---|
title | frontend/src/components/payment-method/styled.ts |
---|
|
Code Block |
---|
| // ... rest of the file
export const StyledNewMethodCardIcon = styled(IconNewMethod)`
${cardIconSize}
`; |
|
Expand |
---|
title | frontend/src/components/payment-method/payment-method-options/types.ts |
---|
|
Code Block |
---|
| export type PaymentMethodOptionProps = {
// ... rest of the file
setIsPaycometNewMethodSelected?: Dispatch<SetStateAction<string>>;
}; |
|
Expand |
---|
title | workspaces/frontend/src/components/payment-method/index.tsx |
---|
|
Add new props in PaymentMethod : Code Block |
---|
|
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: Code Block |
---|
| // ... 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: Code Block |
---|
const CurrentSelectedMethodMap = {
// ... rest of the file
[CurrentSelectedMethodType.ADD_NEW_PAYMENT_METHOD]: SelectedNewMethodOptionItem,
}; |
Pass the new prop in the <PaymentMethodOptions> comp: Code Block |
---|
| <PaymentMethodOptions
setIsPaycometNewMethodSelected={setIsPaycometNewMethodSelected}
/> |
|
Expand |
---|
title | frontend/src/components/payment-method/payment-method-options/payment-method-options.tsx |
---|
|
Add the new prop in PaymentMethodOptions comp: Code Block |
---|
| 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: Code Block |
---|
| if (setIsPaycometSodexoOptionSelected) {
setIsPaycometNewMethodSelected('');
} |
Add a new handle for our new method (using useCallback) where we’ll clear the other methods and set your: Code Block |
---|
| 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: |
...
Code Block |
---|
| 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: Code Block |
---|
| {!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 |
...
...
...
...
Return the new values from the useOrderPayment
hook
...
Code Block |
---|
| const paymentMethodListTypeOrder = |
|
...
...
...
...
...
...
...
method
function filterMethodByType(typeOrder: |
|
...
string, paymentMethods: IPaymentMethod[]): IPaymentMethod[] {
|
|
...
...
Pass down the values to the <PaymentMethod> comp
...
(typeOrder) {
// rest of the code
|
|
...
...
...
...
return paymentMethods.filter(
|
|
...
...
...
...
...
p => !p?.transient && p?.newMethod && p.accountIdentifier |
|
...
...
...
Add a new logic to the filter !enableSortPaymentMethods && unSelectedMethods.filter(isNotLocalWallet)... to now show our payment method if delivery mode is not selected: Code Block |
---|
| const isDelivery |
|
...
= serverOrder?.delivery;
const |
|
...
isNotLocalWallet = (method: |
|
...
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:
...
title | frontend/src/state/payment/types.ts |
---|
Code Block |
---|
|
export interface IPaymentMethod {
// ... rest of the code
newPaymentMethod?: boolean | null;
} |
Add new file in frontend/src/components/payment-method-option/
as the following example:
Expand |
---|
title | frontend/src/components/payment-method-option/new-payment-method.tsx (example of new file) |
---|
|
Code Block |
---|
| 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; |
|
...
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 => { |
|
...
cart/payment/order-payment/paycomet-hosted- |
|
...
...
payment/paycomet-hosted-page-payment.tsx |
|
...
Return the new values from the useOrderPayment hook |
...
...
...
...
...
code
setIsPaycometNewMethodSelected,
|
|
...
...
...
...
isPaycometNewMethodSelected,
} |
|
...
...
...
...
...
...
Pass down the values to the <PaymentMethod> comp Code Block |
---|
| // rest of the code
<PaymentInfoBackground $hostedPayment>
|
|
...
{showPaymentMethodSelection && (
|
|
...
<PaymentMethod
// rest of the code
|
|
...
setIsPaycometNewMethodSelected={setIsPaycometNewMethodSelected} |
|
...
...
...
...
...
isPaycometNewMethodSelected={isPaycometNewMethodSelected} // |
|
...
...
onResult={onPaymentOutcome}
/>
|
|
...
Task 5: Adjust account payment method lists to deal with the new method
...
title | 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:
...
...
Task 4: Create and add a new method in payment-method-option structure
Image AddedAdd 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:
Expand |
---|
title | frontend/src/state/payment/types.ts |
---|
|
Code Block |
---|
| export interface IPaymentMethod {
// rest of the code
newPaymentMethod?: boolean | null;
} |
|
Expand |
---|
title | frontend/src/components/payment-method-option/new-payment-method.tsx (example of new file) |
---|
|
Code Block |
---|
| 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; |
|
Expand |
---|
title | frontend/src/components/payment-method-option/index.tsx |
---|
|
Adjust the RenderMethodType and add the new method created above Code Block |
---|
|
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
Image Added Expand |
---|
title | 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: Code Block |
---|
| 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
Image Added Image Added Suggestion of implementation:
For the payment reminder tip we have some options:
Image AddedCreate new component as the <CashPaymentReminder />
that we have today for the new condition/new text
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
Image AddedToday, 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:
Expand |
---|
title | packages/emails/src/utils.ts |
---|
|
Code Block |
---|
| 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:
Note |
---|
The example displays a LiquidJS template, which is similar but not equal to Braze templates. Braze access properties using the syntax {{event_properties.${language}}} |
Expand |
---|
title | LiquidJS template example |
---|
|
Code Block |
---|
{% 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>
|
|