TD-378: Add payment terminal selector form (#135)

This commit is contained in:
Ildar Galeev 2022-08-23 15:54:29 +03:00 committed by GitHub
parent b64e662f71
commit 016a897ede
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 223 additions and 40 deletions

View File

@ -27,7 +27,7 @@ export * from './payment-error';
export * from './invoice-status';
export * from './samsungpay';
export * from './mobile-commerce';
export * from './online-banking';
export * from './service-provider';
export * from './shortened-url-params';
export * from './shortened-url';
export * from './service-provider-metadata';

View File

@ -8,8 +8,8 @@ export interface MetadataTextLocalization {
}
export interface ServiceProviderMetadataField {
name: string;
type: JSX.IntrinsicElements['input']['type'];
name: string;
required: boolean;
pattern?: string;
localization?: MetadataTextLocalization;

View File

@ -25,6 +25,7 @@ import { RedirectForm } from './redirect-form';
import { PaymentTerminalBankCardForm } from './payment-terminal-bank-card-form';
import { PaymentTerminalForm } from './payment-terminal-form';
import { QrCodeInteractionForm } from './qr-code-interaction-form';
import { PaymentTerminalSelectorForm } from './payment-terminal-selector-form';
const Container = styled.div`
padding: 0 5px;
@ -215,6 +216,8 @@ class FormContainerDef extends React.Component<FormContainerProps, { height: num
return <PaymentTerminalBankCardForm key={name} />;
case FormName.paymentTerminalForm:
return <PaymentTerminalForm key={name} />;
case FormName.paymentTerminalSelector:
return <PaymentTerminalSelectorForm key={name} />;
case FormName.qrCodeInteractionForm:
return <QrCodeInteractionForm key={name} />;
default:

View File

@ -0,0 +1,11 @@
import * as React from 'react';
import { KnownProviderCategories } from 'checkout/state';
export const CategoryContent: React.FC<{ category: KnownProviderCategories }> = ({ category }) => {
switch (category) {
case 'netbanking':
return <img src="/assets/inb-logo.jpg" height="68px" width="106px"></img>;
}
return <div>{category}</div>;
};

View File

@ -0,0 +1,17 @@
import * as React from 'react';
import { PaymentTerminalPaymentMethod } from 'checkout/state';
import { MetadataContent } from './metadata-content';
import { CategoryContent } from './category-content';
export const Content: React.FC<{ method: PaymentTerminalPaymentMethod; localeCode: string }> = ({
method,
localeCode
}) => {
if (method.serviceProviders.length === 1) {
return <MetadataContent serviceProvider={method.serviceProviders[0]} localeCode={localeCode} />;
}
if (method.serviceProviders.length > 1) {
return <CategoryContent category={method.category} />;
}
};

View File

@ -0,0 +1,17 @@
import * as React from 'react';
import { ServiceProvider } from 'checkout/backend';
import { getMetadata, MetadataLogo, MetadataTitle } from 'checkout/components';
export const MetadataContent: React.FC<{ serviceProvider: ServiceProvider; localeCode: string }> = ({
serviceProvider,
localeCode
}) => {
const { logo, title } = getMetadata(serviceProvider);
return (
<>
{title && <MetadataTitle metadata={title} localeCode={localeCode} />}
{logo && <MetadataLogo metadata={logo} />}
</>
);
};

View File

@ -5,12 +5,14 @@ import {
FormName,
KnownProviderCategories,
PaymentTerminalFormInfo,
PaymentTerminalPaymentMethod
PaymentTerminalPaymentMethod,
PaymentTerminalSelectorFormInfo
} from 'checkout/state';
import { getMetadata, MetadataLogo, MetadataTitle, PaymentMethodItemContainer } from 'checkout/components/ui';
import { getMetadata, PaymentMethodItemContainer } from 'checkout/components/ui';
import { PayAction, SetFormInfoAction } from './types';
import { payWithPaymentTerminal } from './pay-with-payment-terminal';
import { ServiceProvider, ServiceProviderContactInfo } from 'checkout/backend';
import { Content } from './content';
export interface PaymentTerminalMethodItemProps {
method: PaymentTerminalPaymentMethod;
@ -21,8 +23,11 @@ export interface PaymentTerminalMethodItemProps {
phoneNumberPrefilled: boolean;
}
const toPaymentTerminal = (category: KnownProviderCategories, setFormInfo: SetFormInfoAction) =>
setFormInfo(new PaymentTerminalFormInfo(category, FormName.paymentMethods));
const toPaymentTerminalSelector = (category: KnownProviderCategories, setFormInfo: SetFormInfoAction) =>
setFormInfo(new PaymentTerminalSelectorFormInfo(category, FormName.paymentMethods));
const toPaymentTerminal = (serviceProviderID: string, setFormInfo: SetFormInfoAction) =>
setFormInfo(new PaymentTerminalFormInfo(serviceProviderID, FormName.paymentMethods));
const isRequiredEmail = (contactInfo: ServiceProviderContactInfo, emailPrefilled: boolean): boolean =>
!isNil(contactInfo) && contactInfo.email === true && !emailPrefilled;
@ -44,15 +49,21 @@ const isRequiredPaymentTerminalForm = (
};
const provideMethod = (
serviceProvider: ServiceProvider,
method: PaymentTerminalPaymentMethod,
pay: PayAction,
setFormInfo: SetFormInfoAction,
emailPrefilled: boolean,
phoneNumberPrefilled: boolean
) => {
return isRequiredPaymentTerminalForm(serviceProvider, emailPrefilled, phoneNumberPrefilled)
? toPaymentTerminal(serviceProvider.category as KnownProviderCategories, setFormInfo)
: payWithPaymentTerminal(serviceProvider.id, pay);
if (method.serviceProviders.length === 1) {
const serviceProvider = method.serviceProviders[0];
return isRequiredPaymentTerminalForm(serviceProvider, emailPrefilled, phoneNumberPrefilled)
? toPaymentTerminal(serviceProvider.id, setFormInfo)
: payWithPaymentTerminal(serviceProvider.id, pay);
}
if (method.serviceProviders.length > 1) {
return toPaymentTerminalSelector(method.category, setFormInfo);
}
};
export const PaymentTerminalMethodItem: React.FC<PaymentTerminalMethodItemProps> = ({
@ -62,15 +73,10 @@ export const PaymentTerminalMethodItem: React.FC<PaymentTerminalMethodItemProps>
localeCode,
emailPrefilled,
phoneNumberPrefilled
}) => {
const serviceProvider = method.serviceProviders[0];
const { logo, title } = getMetadata(serviceProvider);
return (
<PaymentMethodItemContainer
id={`${serviceProvider.id}-payment-method-item`}
onClick={() => provideMethod(serviceProvider, pay, setFormInfo, emailPrefilled, phoneNumberPrefilled)}>
{title && <MetadataTitle metadata={title} localeCode={localeCode} />}
{logo && <MetadataLogo metadata={logo} />}
</PaymentMethodItemContainer>
);
};
}) => (
<PaymentMethodItemContainer
id={`${Math.floor(Math.random() * 100)}-payment-method-item`}
onClick={() => provideMethod(method, pay, setFormInfo, emailPrefilled, phoneNumberPrefilled)}>
<Content method={method} localeCode={localeCode} />
</PaymentMethodItemContainer>
);

View File

@ -31,8 +31,8 @@ export const PaymentTerminalMethodItems: React.FC<PaymentTerminalMethodItemsProp
case KnownProviderCategories.PIX:
case KnownProviderCategories.PaymentTerminal:
case KnownProviderCategories.DigitalWallet:
case KnownProviderCategories.OnlineBanking:
case KnownProviderCategories.NetBanking:
case KnownProviderCategories.OnlineBanking:
return (
<PaymentTerminalMethodItem
method={method}

View File

@ -18,9 +18,9 @@ import { PayButton } from '../pay-button';
import { FormGroup } from '../form-group';
import {
getActiveModalFormSelector,
getAvailableTerminalPaymentMethodSelector,
getInitConfigSelector,
getModelSelector
getModelSelector,
getServiceProviderSelector
} from 'checkout/selectors';
import { getMetadata, MetadataField, MetadataLogo } from 'checkout/components/ui';
import { toAmountConfig, toEmailConfig, toPhoneNumberConfig } from '../fields-config';
@ -38,17 +38,15 @@ const Container = styled.div`
`;
const PaymentTerminalFormRef: React.FC<InjectedFormProps> = ({ submitFailed, initialize, handleSubmit }) => {
const { providerID, paymentStatus } = useAppSelector<PaymentTerminalFormInfo>(getActiveModalFormSelector);
const serviceProvider = useAppSelector(getServiceProviderSelector(providerID));
const { form, contactInfo, logo } = getMetadata(serviceProvider);
const initConfig = useAppSelector(getInitConfigSelector);
const model = useAppSelector(getModelSelector);
const { category } = useAppSelector<PaymentTerminalFormInfo>(getActiveModalFormSelector);
const paymentMethod = useAppSelector(getAvailableTerminalPaymentMethodSelector(category));
const serviceProvider = paymentMethod?.serviceProviders[0];
const formValues = useAppSelector((s) => get(s.form, 'paymentTerminalForm.values'));
const { form, contactInfo, logo } = getMetadata(serviceProvider);
const { paymentStatus } = useAppSelector<PaymentTerminalFormInfo>(getActiveModalFormSelector);
const amount = toAmountConfig(initConfig, model.invoiceTemplate);
const email = toEmailConfig(initConfig.email);
const phoneNumber = toPhoneNumberConfig(initConfig.phoneNumber);
const formValues = useAppSelector((s) => get(s.form, 'paymentTerminalForm.values'));
const dispatch = useAppDispatch();
useEffect(() => {

View File

@ -0,0 +1 @@
export * from './payment-terminal-selector-form';

View File

@ -0,0 +1,53 @@
import * as React from 'react';
import styled from 'checkout/styled-components';
import { useAppDispatch, useAppSelector } from 'checkout/configure-store';
import { Header } from '../header';
import {
FormName,
KnownProviderCategories,
PaymentTerminalFormInfo,
PaymentTerminalSelectorFormInfo
} from 'checkout/state';
import {
getActiveModalFormSelector,
getAvailableTerminalPaymentMethodSelector,
getLocaleSelector
} from 'checkout/selectors';
import { ServiceProviderPane } from './service-provider-pane';
import { goToFormInfo } from 'checkout/actions';
import { Locale } from 'checkout/locale';
const Container = styled.div`
min-height: 300px;
display: flex;
flex-direction: column;
`;
const Grid = styled.div`
display: grid;
grid-gap: 16px;
grid-template-columns: 1fr 1fr;
`;
const navigate = (providerID: string) =>
goToFormInfo(new PaymentTerminalFormInfo(providerID, FormName.paymentTerminalSelector));
const toHeader = (locale: Locale, category: KnownProviderCategories) => locale[`form.header.${category}.label`];
export const PaymentTerminalSelectorForm: React.FC = () => {
const locale = useAppSelector(getLocaleSelector);
const { category } = useAppSelector<PaymentTerminalSelectorFormInfo>(getActiveModalFormSelector);
const paymentMethod = useAppSelector(getAvailableTerminalPaymentMethodSelector(category));
const dispatch = useAppDispatch();
return (
<Container>
<Header title={toHeader(locale, category)} />
<Grid>
{paymentMethod?.serviceProviders.map((p, i) => (
<ServiceProviderPane key={i} serviceProvider={p} onClick={(id) => dispatch(navigate(id))} />
))}
</Grid>
</Container>
);
};

View File

@ -0,0 +1,55 @@
import * as React from 'react';
import styled from 'checkout/styled-components';
import { ServiceProvider, ServiceProviderIconMetadata } from 'checkout/backend';
import { getMetadata, MetadataLogo } from 'checkout/components';
const PaneContainer = styled.div`
cursor: pointer;
height: 64px;
border-radius: 8px;
border: 1px solid ${({ theme }) => theme.color.neutral[0.2]};
:hover {
border-color: ${({ theme }) => theme.color.primary[1]};
}
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
`;
const PaneLabel = styled.p`
font-weight: 400;
font-size: 12px;
line-height: 20px;
margin: 0;
`;
const PaneLogoContainer = styled.div`
height: 32px;
display: flex;
align-items: center;
justify-content: center;
`;
const PaneLogo: React.FC<{ logo: ServiceProviderIconMetadata }> = ({ logo }) => (
<PaneLogoContainer>
<MetadataLogo metadata={logo} />
</PaneLogoContainer>
);
export const ServiceProviderPane: React.FC<{ serviceProvider: ServiceProvider; onClick: (id: string) => void }> = ({
serviceProvider,
onClick
}) => {
const { logo } = getMetadata(serviceProvider);
return (
<PaneContainer onClick={() => onClick(serviceProvider.id)}>
{logo && <PaneLogo logo={logo} />}
<PaneLabel>{serviceProvider.brandName}</PaneLabel>
</PaneContainer>
);
};

View File

@ -12,12 +12,13 @@ import {
PaymentTerminalPaymentMethod,
KnownProviderCategories,
PaymentTerminalBankCardFormInfo,
PaymentTerminalFormInfo
PaymentTerminalFormInfo,
PaymentTerminalSelectorFormInfo
} from 'checkout/state';
import { BankCardTokenProvider } from 'checkout/backend/model';
import { assertUnreachable } from 'checkout/utils';
const toPaymentTerminalForms = ({ category }: PaymentTerminalPaymentMethod) => {
const toPaymentTerminalForms = ({ category, serviceProviders }: PaymentTerminalPaymentMethod) => {
switch (category) {
case KnownProviderCategories.BankCard:
return new PaymentTerminalBankCardFormInfo();
@ -27,7 +28,10 @@ const toPaymentTerminalForms = ({ category }: PaymentTerminalPaymentMethod) => {
case KnownProviderCategories.PIX:
case KnownProviderCategories.PaymentTerminal:
case KnownProviderCategories.OnlineBanking:
return new PaymentTerminalFormInfo(category);
if (serviceProviders.length === 1) {
return new PaymentTerminalFormInfo(serviceProviders[0].id);
}
return new PaymentTerminalSelectorFormInfo(category);
default:
assertUnreachable(category);
return null;

View File

@ -1,3 +1,5 @@
import isNil from 'lodash-es/isNil';
import {
PaymentMethodName,
State,
@ -9,6 +11,9 @@ import {
export const getAvailableTerminalPaymentMethodSelector = (category: KnownProviderCategories) => (
state: State
): PaymentTerminalPaymentMethod | null => {
if (isNil(category)) {
return null;
}
const found = state.availablePaymentMethods.find((m) => {
if (m.name !== PaymentMethodName.PaymentTerminal) {
return false;

View File

@ -15,6 +15,7 @@ export enum FormName {
redirectForm = 'redirectForm',
paymentTerminalBankCard = 'paymentTerminalBankCard',
paymentTerminalForm = 'paymentTerminalForm',
paymentTerminalSelector = 'paymentTerminalSelector',
qrCodeInteractionForm = 'qrCodeInteractionForm'
}

View File

@ -15,3 +15,4 @@ export * from './redirect-form-info';
export * from './payment-terminal-bank-card-form-info';
export * from './payment-terminal-form-info';
export * from './qr-code-interaction-form-info';
export * from './payment-terminal-selector-form-info';

View File

@ -1,4 +1,3 @@
import { KnownProviderCategories } from 'checkout/state/payment-method';
import { FormInfo, FormName } from './form-info';
import { PaymentStatus } from './payment-status';
@ -7,7 +6,7 @@ export class PaymentTerminalFormInfo extends FormInfo {
active = true;
paymentStatus = PaymentStatus.pristine;
constructor(public category: KnownProviderCategories, previous?: FormName) {
constructor(public providerID: string, previous?: FormName) {
super(previous);
}
}

View File

@ -0,0 +1,11 @@
import { KnownProviderCategories } from 'checkout/state';
import { FormInfo, FormName } from './form-info';
export class PaymentTerminalSelectorFormInfo extends FormInfo {
name = FormName.paymentTerminalSelector;
active = true;
constructor(public category: KnownProviderCategories, previous?: FormName) {
super(previous);
}
}

BIN
src/assets/inb-logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -7,7 +7,7 @@
"info.modal.no.available.payment.method": "No available payment methods",
"footer.pay.label": "Secure payment with",
"form.header.payment.methods.label": "Please choose a payment method",
"form.header.payment.method.groups.label": "Please choose a payment method",
"form.header.netbanking.label": "Please choose a bank",
"form.header.pay.card.label": "Pay with a card",
"form.header.pay.mobile.commerce.label": "Pay with mobile phone account",
"form.header.pay.upi.label": "Unified payment interface",

View File

@ -7,7 +7,7 @@
"info.modal.no.available.payment.method": "利用できる支払方法はありません",
"footer.pay.label": "Secure payment with",
"form.header.payment.methods.label": "お支払い方法を選択してください",
"form.header.payment.method.groups.label": "お支払い方法を選択してください",
"form.header.netbanking.label": "Please choose a bank",
"form.header.pay.card.label": "カードで支払う",
"form.header.pay.mobile.commerce.label": "携帯電話アカウントで支払います。",
"form.header.pay.upi.label": "Unified payment interface",

View File

@ -7,7 +7,7 @@
"info.modal.no.available.payment.method": "Sem opções de pagamento disponível",
"footer.pay.label": "Secure payment with",
"form.header.payment.methods.label": "Por favor escolha seu método de pagamento",
"form.header.payment.method.groups.label": "Por favor escolha seu método de pagamento",
"form.header.netbanking.label": "Please choose a bank",
"form.header.pay.card.label": "Pay with a card",
"form.header.pay.mobile.commerce.label": "Pay with mobile phone account",
"form.header.pay.upi.label": "Unified payment interface",

View File

@ -7,7 +7,7 @@
"info.modal.no.available.payment.method": "Отсутствуют доступные методы оплаты",
"footer.pay.label": "Безопасная оплата с",
"form.header.payment.methods.label": "Выберите способ оплаты",
"form.header.payment.method.groups.label": "Выберите способ оплаты",
"form.header.netbanking.label": "Выберите банк",
"form.header.pay.card.label": "Оплата банковской картой",
"form.header.pay.mobile.commerce.label": "Оплата со счета телефона",
"form.header.pay.upi.label": "Unified payment interface",