Terminal qiwi support. (#202)

This commit is contained in:
Ildar Galeev 2018-01-26 23:32:50 +07:00 committed by GitHub
parent 89528bb8e6
commit 573963d73a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
153 changed files with 1751 additions and 915 deletions

View File

@ -20,13 +20,14 @@
"date-fns": "~1.28.5",
"did-you-mean": "0.0.1",
"ismobilejs": "~0.4.1",
"libphonenumber-js": "~1.0.3",
"lodash": "~4.17.4",
"react": "~15.5.4",
"react-dom": "~15.5.4",
"react-redux": "~5.0.5",
"react-transition-group": "~1.1.3",
"redux": "~3.7.2",
"redux-form": "~7.2.0",
"redux-form": "~7.2.1",
"redux-thunk": "~2.2.0",
"tokenizer": "git+ssh://git@github.com/rbkmoney/tokenizer.js.git#892793a1cdb035db16de6a067e43bbca2a5215e1",
"uri-template": "~1.0.1",

View File

@ -1,2 +1 @@
export * from './to-card-form-info';
export * from './to-modal-interaction';

View File

@ -1,23 +1,15 @@
import {
BrowserPostRequest,
BrowserRequest,
ChangeType,
Event,
InteractionType,
PaymentInteractionRequested,
Redirect,
RequestType
} from 'checkout/backend';
import { ModalInteraction } from 'checkout/state';
import { getLastChange } from 'checkout/utils';
const getRedirect = (redirect: Redirect): BrowserPostRequest => {
if (redirect.request.requestType === RequestType.BrowserPostRequest) {
return redirect.request as BrowserPostRequest;
}
throw new Error('Unsupported user interaction browser request type');
};
const toRequest = (events: Event[]): BrowserPostRequest => {
const toRequest = (events: Event[]): BrowserRequest => {
const change = getLastChange(events);
if (!change || change.changeType !== ChangeType.PaymentInteractionRequested) {
throw new Error('ChangeType must be PaymentInteractionRequested');
@ -25,10 +17,11 @@ const toRequest = (events: Event[]): BrowserPostRequest => {
const interaction = (change as PaymentInteractionRequested).userInteraction;
switch (interaction.interactionType) {
case InteractionType.Redirect:
return getRedirect(interaction as Redirect);
return (interaction as Redirect).request;
case InteractionType.PaymentTerminalReceipt:
throw new Error('Unsupported user interaction PaymentTerminalReceipt');
}
throw new Error('Unsupported InteractionType');
};
export const toModalInteraction = (events: Event[]) => new ModalInteraction(toRequest(events), true);

View File

@ -0,0 +1,9 @@
import { AbstractAction, TypeKeys } from 'checkout/actions';
export interface ForgetPaymentAttempt extends AbstractAction {
type: TypeKeys.FORGET_PAYMENT_ATTEMPT;
}
export const forgetPaymentAttempt = (): ForgetPaymentAttempt => ({
type: TypeKeys.FORGET_PAYMENT_ATTEMPT
});

View File

@ -0,0 +1,22 @@
import { FormInfo} from 'checkout/state';
import { AbstractAction, TypeKeys } from 'checkout/actions';
export enum Direction {
back = 'back',
forward = 'forward'
}
export interface GoToPayload {
formInfo: FormInfo;
direction: Direction;
}
export interface GoToFormInfo extends AbstractAction<GoToPayload> {
type: TypeKeys.GO_TO_FORM_INFO;
payload: GoToPayload;
}
export const goToFormInfo = (formInfo: FormInfo, direction: Direction = Direction.forward): GoToFormInfo => ({
type: TypeKeys.GO_TO_FORM_INFO,
payload: {formInfo, direction}
});

View File

@ -3,6 +3,6 @@ export * from './set-modal-state';
export * from './view-info-actions';
export * from './set-modal-from-events';
export * from './prepare-payment-actions';
export * from './set-form-info';
export * from './set-error-form-info-action';
export * from './go-to-form-info-action';
export * from './modal-interaction-polling-status-action';
export * from './forget-payment-attempt';

View File

@ -1,27 +1,38 @@
import { ChangeType } from 'checkout/backend';
import { ChangeType, PaymentMethod, PaymentMethodName } from 'checkout/backend';
import {
PaymentMethodsFormInfo,
ModalForms,
ModalState,
ModelState,
ResultFormInfo,
ResultType
ResultType,
CardFormInfo
} from 'checkout/state';
import { TypeKeys } from 'checkout/actions';
import { SetModalState } from './set-modal-state';
import { InitConfig } from 'checkout/config';
import { toCardFormInfo, toModalInteraction } from './converters';
import { toModalInteraction } from './converters';
import { getLastChange } from 'checkout/utils';
const isMultiMethods = (c: InitConfig, m: ModelState) => c.terminals && m.paymentMethods.length > 1;
const checkPaymentMethodsConfig = (c: InitConfig, methods: PaymentMethod[]): boolean =>
methods.reduce((acc, current): boolean => {
switch (current.method) {
// case PaymentMethodName.PaymentTerminal:
// return acc || c.terminals;
case PaymentMethodName.DigitalWallet:
return acc || c.wallets;
}
}, false);
const isMultiMethods = (c: InitConfig, m: ModelState) => m.paymentMethods.length > 1 && checkPaymentMethodsConfig(c, m.paymentMethods);
const toInitialState = (c: InitConfig, m: ModelState): ModalState => {
const formInfo = isMultiMethods(c, m) ? new PaymentMethodsFormInfo(true) : toCardFormInfo(c, m.invoiceTemplate);
const formInfo = isMultiMethods(c, m) ? new PaymentMethodsFormInfo() : new CardFormInfo();
return new ModalForms([formInfo], true);
};
const toInitialModalResult = (): ModalState => {
const formInfo = new ResultFormInfo(ResultType.processed, true);
const formInfo = new ResultFormInfo(ResultType.processed);
return new ModalForms([formInfo], true);
};

View File

@ -1,7 +0,0 @@
import { SetFormInfo, TypeKeys } from 'checkout/actions';
import { ResultFormInfo, ResultType } from 'checkout/state';
export const setErrorFormInfo = (): SetFormInfo => ({
type: TypeKeys.SET_FORM_INFO,
payload: new ResultFormInfo(ResultType.error, true)
});

View File

@ -1,7 +0,0 @@
import { FormInfo } from 'checkout/state';
import { AbstractAction, TypeKeys } from 'checkout/actions';
export interface SetFormInfo extends AbstractAction<FormInfo> {
type: TypeKeys.SET_FORM_INFO;
payload: FormInfo;
}

View File

@ -1,5 +1,5 @@
import { ResultFormInfo, ResultType } from 'checkout/state';
import { SetFormInfo, TypeKeys } from 'checkout/actions';
import { Direction, GoToFormInfo, TypeKeys } from 'checkout/actions';
import { ChangeType, Event } from 'checkout/backend';
import { SetModalState } from './set-modal-state';
import { toModalInteraction } from './converters';
@ -16,13 +16,16 @@ const prepareFromEvents = (events: Event[]): SetStateFromEvents => {
case ChangeType.PaymentStatusChanged:
case ChangeType.InvoiceStatusChanged:
return {
type: TypeKeys.SET_FORM_INFO,
payload: new ResultFormInfo(ResultType.processed, true)
type: TypeKeys.GO_TO_FORM_INFO,
payload: {
formInfo: new ResultFormInfo(ResultType.processed),
direction: Direction.forward
}
};
}
throw new Error('Unhandled invoice changeType');
};
export type SetStateFromEvents = SetFormInfo | SetModalState;
export type SetStateFromEvents = GoToFormInfo | SetModalState;
export const setModalFromEvents = (events: Event[]): SetStateFromEvents => prepareFromEvents(events);

View File

@ -1,34 +1,21 @@
import { AbstractAction, TypeKeys } from 'checkout/actions';
import { FormName } from 'checkout/state';
interface Meta {
formName: FormName;
}
export interface SetViewInfoError extends AbstractAction<boolean, Meta> {
export interface SetViewInfoError extends AbstractAction<boolean> {
type: TypeKeys.SET_VIEW_INFO_ERROR;
payload: boolean;
meta: Meta;
}
export const setViewInfoError = (hasError: boolean, formName: FormName): SetViewInfoError => ({
export const setViewInfoError = (hasError: boolean): SetViewInfoError => ({
type: TypeKeys.SET_VIEW_INFO_ERROR,
payload: hasError,
meta: {
formName
}
payload: hasError
});
export interface SetViewInfoInProcess extends AbstractAction<boolean, Meta> {
type: TypeKeys.SET_VIEW_INFO_IN_PROCESS;
payload: boolean;
meta: Meta;
export interface SetViewInfoHeight extends AbstractAction<number> {
type: TypeKeys.SET_VIEW_INFO_HEIGHT;
payload: number;
}
export const setViewInfoInProcess = (inProcess: boolean, formName: FormName): SetViewInfoInProcess => ({
type: TypeKeys.SET_VIEW_INFO_IN_PROCESS,
payload: inProcess,
meta: {
formName
}
export const setViewInfoHeight = (height: number): SetViewInfoHeight => ({
type: TypeKeys.SET_VIEW_INFO_HEIGHT,
payload: height
});

View File

@ -1,14 +1,24 @@
import {
PaymentToolType,
createPaymentResource as capiRequest,
PaymentResource
PaymentResource,
DigitalWalletType
} from 'checkout/backend';
import { CardFormValues } from 'checkout/state';
import { CardFormValues, WalletFormValues } from 'checkout/state';
import { PaymentSubject } from './payment-subject';
const replaceSpaces = (str: string): string => str.replace(/\s+/g, '');
export const createPaymentResource = (s: PaymentSubject, endpoint: string, v: CardFormValues): Promise<PaymentResource> => {
export const createPaymentResourceDigitalWalletQiwi = (s: PaymentSubject, endpoint: string, v: WalletFormValues): Promise<PaymentResource> => {
const paymentTool = {
paymentToolType: PaymentToolType.DigitalWalletData,
digitalWalletType: DigitalWalletType.DigitalWalletQIWI,
phoneNumber: replaceSpaces(v.phone)
};
return capiRequest(endpoint, s.accessToken, paymentTool);
};
export const createPaymentResourceCardData = (s: PaymentSubject, endpoint: string, v: CardFormValues): Promise<PaymentResource> => {
const cardNumber = replaceSpaces(v.cardNumber);
const expDate = replaceSpaces(v.expireDate);
const paymentTool = {

View File

@ -1,12 +1,11 @@
import { PaymentResource, createPayment as capiRequest, FlowType, PayerType } from 'checkout/backend';
import { PaymentResource, createPayment as capiRequest, PayerType } from 'checkout/backend';
import { PaymentSubject } from './payment-subject';
import { PaymentFlow } from 'checkout/backend/model/payment-flow';
export const createPayment = (s: PaymentSubject, endpoint: string, r: PaymentResource, email: string) => {
export const createPayment = (s: PaymentSubject, endpoint: string, r: PaymentResource, email: string, flow: PaymentFlow) => {
const {paymentToolToken, paymentSession} = r;
const request = {
flow: {
type: FlowType.PaymentFlowInstant
},
flow,
payer: {
payerType: PayerType.PaymentResourcePayer,
paymentToolToken,

View File

@ -1,15 +1,37 @@
import { CardFormValues, ConfigState, ModelState } from 'checkout/state';
import { createPayment, createPaymentResource, getPaymentSubject, pollEvents } from './';
import { CardFormValues, ConfigState, ModelState, PayableFormValues, WalletFormValues } from 'checkout/state';
import {
createPayment,
createPaymentResourceCardData,
getPaymentSubject,
pollEvents,
createPaymentResourceDigitalWalletQiwi
} from './';
import { PayActionPayload } from '../pay-action';
import { FlowType, PaymentFlow, PaymentResource } from 'checkout/backend';
import { PaymentSubject } from './payment-subject';
export const pay = (c: ConfigState, m: ModelState, v: CardFormValues): Promise<PayActionPayload> => {
type CreatePaymentResourceFn = (s: PaymentSubject, endpoint: string, v: PayableFormValues) => Promise<PaymentResource>;
const toPaymentFlow = (c: ConfigState): PaymentFlow => {
const instant = {type: FlowType.PaymentFlowInstant};
const hold = {type: FlowType.PaymentFlowHold, onHoldExpiration: c.initConfig.holdExpiration};
return c.initConfig.paymentFlowHold ? hold : instant;
};
const pay = (c: ConfigState, m: ModelState, v: PayableFormValues, createPaymentResourceFn: CreatePaymentResourceFn): Promise<PayActionPayload> => {
const endpoint = c.appConfig.capiEndpoint;
const email = c.initConfig.email || v.email;
return getPaymentSubject(c, m, v.amount).then((subject) =>
createPaymentResource(subject, endpoint, v).then((paymentResource) =>
createPayment(subject, endpoint, paymentResource, email).then(() =>
createPaymentResourceFn(subject, endpoint, v).then((paymentResource) =>
createPayment(subject, endpoint, paymentResource, email, toPaymentFlow(c)).then(() =>
pollEvents(endpoint, subject, m.invoiceEvents).then((events) => ({
invoiceEvents: events,
invoiceAccessToken: subject.accessToken
})))));
};
export const payCardData = (c: ConfigState, m: ModelState, v: CardFormValues): Promise<PayActionPayload> =>
pay(c, m, v, createPaymentResourceCardData);
export const payDigitalWalletQiwi = (c: ConfigState, m: ModelState, v: WalletFormValues): Promise<PayActionPayload> =>
pay(c, m, v, createPaymentResourceDigitalWalletQiwi);

View File

@ -7,6 +7,14 @@ import { IntegrationType } from 'checkout/config';
const pollingRetries = 60;
const pollingTimeout = 300;
const continuePolling = (pollCount: number, retries: number, pollFn: () => any, reject: (reason: any) => any): number => {
const count = pollCount + 1;
count >= retries
? reject({code: 'error.events.timeout'})
: pollFn();
return count;
};
export const pollEvents = (endpoint: string, subject: PaymentSubject, e: Event[]): Promise<Event[]> => {
return new Promise((resolve, reject) => {
let pollCount = 0;
@ -16,17 +24,18 @@ export const pollEvents = (endpoint: string, subject: PaymentSubject, e: Event[]
const lastEventID = (e && !templateIntegration) ? last(e).id : 0;
getInvoiceEvents(endpoint, subject.accessToken, subject.invoiceID, 10, lastEventID).then((events) => {
const change = getLastChange(events);
switch (change.changeType) {
case ChangeType.InvoiceStatusChanged:
case ChangeType.PaymentStatusChanged:
case ChangeType.PaymentInteractionRequested:
resolve(events);
break;
default:
pollCount++;
pollCount >= pollingRetries
? reject({code: 'error.events.timeout'})
: poll();
if (change) {
switch (change.changeType) {
case ChangeType.InvoiceStatusChanged:
case ChangeType.PaymentStatusChanged:
case ChangeType.PaymentInteractionRequested:
resolve(events);
break;
default:
pollCount = continuePolling(pollCount, pollingRetries, poll, reject);
}
} else {
pollCount = continuePolling(pollCount, pollingRetries, poll, reject);
}
}).catch((error) => reject(error));
}, pollingTimeout);

View File

@ -1,8 +1,11 @@
import { Dispatch } from 'redux';
import { CardFormValues, ConfigState, ModelState } from 'checkout/state';
import { CardFormValues, ConfigState, ModelState, WalletFormValues } from 'checkout/state';
import { Event } from 'checkout/backend';
import { AbstractAction, SetErrorAction, TypeKeys } from 'checkout/actions';
import { pay as payOperation } from './operations';
import {
payCardData as payCardDataOperation,
payDigitalWalletQiwi as payDigitalWalletQiwiOperation
} from './operations';
export interface PayActionPayload {
invoiceEvents: Event[];
@ -16,8 +19,19 @@ export interface PayAction extends AbstractAction<PayActionPayload> {
export type PayDispatch = (dispatch: Dispatch<PayAction | SetErrorAction>) => void;
export const pay = (c: ConfigState, m: ModelState, v: CardFormValues): PayDispatch =>
(dispatch) => payOperation(c, m, v)
export const payCardData = (c: ConfigState, m: ModelState, v: CardFormValues): PayDispatch =>
(dispatch) => payCardDataOperation(c, m, v)
.then((payload) => dispatch({
type: TypeKeys.PAY,
payload
}))
.catch((error) => dispatch({
type: TypeKeys.SET_ERROR,
payload: error
}));
export const payDigitalWalletQiwi = (c: ConfigState, m: ModelState, v: WalletFormValues): PayDispatch =>
(dispatch) => payDigitalWalletQiwiOperation(c, m, v)
.then((payload) => dispatch({
type: TypeKeys.PAY,
payload

View File

@ -23,5 +23,9 @@ export const pollInvoiceEvents = (capiEndpoint: string, accessToken: string, eve
.then((event) => dispatch({
type: TypeKeys.POLL_EVENTS,
payload: event
}))
.catch((error) => dispatch({
type: TypeKeys.SET_ERROR,
payload: error
}));
};

View File

@ -6,12 +6,13 @@ export enum TypeKeys {
SET_RESULT = 'SET_RESULT',
SET_ERROR = 'SET_ERROR',
SET_MODAL_STATE = 'SET_MODAL_STATE',
SET_FORM_INFO = 'SET_FORM_INFO',
GO_TO_FORM_INFO = 'GO_TO_FORM_INFO',
PAY = 'PAY',
POLL_EVENTS = 'POLL_EVENTS',
SET_VIEW_INFO_ERROR = 'SET_VIEW_INFO_ERROR',
SET_VIEW_INFO_IN_PROCESS = 'SET_VIEW_INFO_IN_PROCESS',
SET_VIEW_INFO_HEIGHT = 'SET_VIEW_INFO_HEIGHT',
PREPARE_TO_PAY = 'PREPARE_TO_PAY',
PREPARE_TO_RETRY = 'PREPARE_TO_RETRY',
SET_MODAL_INTERACTION_POLLING = 'SET_MODAL_INTERACTION_POLLING'
FORGET_PAYMENT_ATTEMPT = 'FORGET_PAYMENT_ATTEMPT',
SET_MODAL_INTERACTION_POLLING = 'SET_MODAL_INTERACTION_POLLING',
}

View File

@ -1,7 +1,7 @@
import { PaymentSystem } from './payment-system';
import { PaymentMethod } from './payment-method';
import { PaymentMethod, PaymentMethodName } from './payment-method';
export class BankCard extends PaymentMethod {
method: 'BankCard';
method: PaymentMethodName.BankCard;
paymentSystems: PaymentSystem[];
}

View File

@ -0,0 +1,7 @@
import { PaymentSystem } from './payment-system';
import { PaymentMethod, PaymentMethodName } from './payment-method';
export class DigitalWallet extends PaymentMethod {
method: PaymentMethodName.DigitalWallet;
paymentSystems: PaymentSystem[];
}

View File

@ -0,0 +1,4 @@
import { BrowserRequest } from './browser-request';
export class BrowserGetRequest extends BrowserRequest {
}

View File

@ -6,3 +6,4 @@ export * from './payment-terminal-receipt';
export * from './redirect';
export * from './request-type';
export * from './user-interaction';
export * from './browser-get-request';

View File

@ -4,6 +4,7 @@ export * from './access-token';
export * from './invoice';
export * from './invoice-template-details';
export * from './bank-card';
export * from './digital-wallet';
export * from './payment-method';
export * from './payment-system';
export * from './payment-terminal';

View File

@ -1,4 +1,4 @@
export enum HoldExpiration {
export enum HoldExpirationType {
cancel = 'cancel',
capture = 'capture'
}

View File

@ -2,3 +2,4 @@ export * from './flow-type';
export * from './payment-flow';
export * from './payment-flow-instant';
export * from './payment-flow-hold';
export * from './hold-expiration-type';

View File

@ -1,7 +1,8 @@
import { PaymentFlow } from './payment-flow';
import { FlowType } from './flow-type';
import { HoldExpirationType } from './hold-expiration-type';
export class PaymentFlowHold extends PaymentFlow {
type: FlowType.PaymentFlowHold;
onHoldExpiration: 'cancel' | 'capture';
onHoldExpiration: HoldExpirationType;
}

View File

@ -1,3 +1,9 @@
export abstract class PaymentMethod {
method: 'BankCard' | 'PaymentTerminal';
export enum PaymentMethodName {
'BankCard' = 'BankCard',
'PaymentTerminal' = 'PaymentTerminal',
'DigitalWallet' = 'DigitalWallet'
}
export abstract class PaymentMethod {
method: PaymentMethodName;
}

View File

@ -1,6 +1,6 @@
import { PaymentMethod } from './payment-method';
import { PaymentMethod, PaymentMethodName } from './payment-method';
export class PaymentTerminal extends PaymentMethod {
method: 'PaymentTerminal';
method: PaymentMethodName.PaymentTerminal;
providers: 'euroset';
}

View File

@ -0,0 +1,5 @@
import { DigitalWalletDetails } from './digital-wallet-details';
export class DigitalWalletDetailsQiwi extends DigitalWalletDetails {
phoneNumberMask: string;
}

View File

@ -0,0 +1,3 @@
export enum DigitalWalletDetailsType {
DigitalWalletDetailsQIWI = 'DigitalWalletDetailsQIWI'
}

View File

@ -0,0 +1,5 @@
import { DigitalWalletDetailsType } from './digital-wallet-details-type';
export class DigitalWalletDetails {
digitalWalletDetailsType: DigitalWalletDetailsType;
}

View File

@ -0,0 +1,3 @@
export * from './digital-wallet-details';
export * from './digital-wallet-details-qiwi';
export * from './digital-wallet-details-type';

View File

@ -2,3 +2,5 @@ export * from './payment-tool-details';
export * from './payment-tool-details-bank-card';
export * from './payment-tool-details-payment-terminal';
export * from './payment-tool-details-type';
export * from './payment-tool-details-digital-wallet';
export * from './digital-wallet-details';

View File

@ -1,8 +1,6 @@
import { PaymentToolDetails } from './payment-tool-details';
import { PaymentToolDetailsType } from './payment-tool-details-type';
export class PaymentToolDetailsBankCard extends PaymentToolDetails {
detailsType: PaymentToolDetailsType.PaymentToolDetailsBankCard;
cardNumberMask: string;
paymentSystem: string;
}

View File

@ -0,0 +1,11 @@
import { PaymentToolDetails } from './payment-tool-details';
import { DigitalWalletDetails, DigitalWalletDetailsType } from './digital-wallet-details';
import { PaymentToolDetailsType } from './payment-tool-details-type';
import { applyMixins } from 'checkout/utils';
export class PaymentToolDetailsDigitalWallet implements PaymentToolDetails, DigitalWalletDetails {
detailsType: PaymentToolDetailsType;
digitalWalletDetailsType: DigitalWalletDetailsType;
}
applyMixins(PaymentToolDetailsDigitalWallet, [PaymentToolDetails, DigitalWalletDetails]);

View File

@ -1,7 +1,5 @@
import { PaymentToolDetails } from './payment-tool-details';
import { PaymentToolDetailsType } from './payment-tool-details-type';
export class PaymentToolDetailsPaymentTerminal extends PaymentToolDetails {
detailsType: PaymentToolDetailsType.PaymentToolDetailsPaymentTerminal;
provider: string;
}

View File

@ -1,4 +1,5 @@
export enum PaymentToolDetailsType {
PaymentToolDetailsBankCard = 'PaymentToolDetailsBankCard',
PaymentToolDetailsPaymentTerminal = 'PaymentToolDetailsPaymentTerminal'
PaymentToolDetailsPaymentTerminal = 'PaymentToolDetailsPaymentTerminal',
PaymentToolDetailsDigitalWallet = 'PaymentToolDetailsDigitalWallet'
}

View File

@ -0,0 +1,6 @@
import { PaymentTool } from '../payment-tool';
import { DigitalWalletType } from './digital-wallet-type';
export class DigitalWalletData extends PaymentTool {
digitalWalletType: DigitalWalletType;
}

View File

@ -0,0 +1,5 @@
import { DigitalWalletData } from './';
export class DigitalWalletQiwi extends DigitalWalletData {
phoneNumber: string;
}

View File

@ -0,0 +1,3 @@
export enum DigitalWalletType {
DigitalWalletQIWI = 'DigitalWalletQIWI'
}

View File

@ -0,0 +1,3 @@
export * from './digital-wallet-data';
export * from './digital-wallet-qiwi';
export * from './digital-wallet-type';

View File

@ -2,3 +2,4 @@ export * from './card-data';
export * from './payment-terminal-data';
export * from './payment-tool';
export * from './payment-tool-type';
export * from './digital-wallet-data';

View File

@ -1,4 +1,5 @@
export enum PaymentToolType {
CardData = 'CardData',
PaymentTerminalData = 'PaymentTerminalData'
PaymentTerminalData = 'PaymentTerminalData',
DigitalWalletData = 'DigitalWalletData'
}

View File

@ -24,13 +24,13 @@ const mapDispatchToProps = (dispatch: Dispatch<any>) => ({
class AppDef extends React.Component<AppProps> {
componentDidMount() {
componentWillMount() {
this.props.loadConfig(this.props.config.initConfig.locale);
}
componentWillReceiveProps(props: AppProps) {
const {config, model, modalReady} = props;
if (config.ready && model.status === ModelStatus.none) {
const {config, model, modalReady, error} = props;
if (config.ready && model.status === ModelStatus.none && !error) {
props.initModel(props.config);
}
if (!modalReady && model.status === ModelStatus.initialized) {

View File

@ -1,15 +1,5 @@
export const appearContainer: string;
export const popup: string;
export const enterContainer: string;
export const leaveContainer: string;
export const popout: string;
export const container: string;
export const fadein: string;
export const fadeout: string;
export const slidedown: string;
export const slideup: string;
export const growth: string;
export const rotatein: string;
export const rotateout: string;
export const shake: string;
export const animationContainer: string;

View File

@ -6,10 +6,21 @@ import * as styles from './modal-container.scss';
import { Modal } from './modal';
import { Footer } from './footer';
import { UserInteractionModal } from './user-interaction-modal';
import { ErrorStatus, ModalName, ModalState, ModelState, State, ModelStatus, ModalInteraction } from 'checkout/state';
import {
accept, pollInvoiceEvents, setErrorFormInfo, acceptError, setModalFromEvents,
setModalInteractionPollingStatus
ErrorStatus,
ModalName,
ModalState,
ModelState,
State,
ModelStatus,
ModalInteraction,
FormInfo,
ResultFormInfo,
ResultType
} from 'checkout/state';
import {
accept, pollInvoiceEvents, acceptError, setModalFromEvents,
setModalInteractionPollingStatus, goToFormInfo
} from 'checkout/actions';
import { AppConfig, Event } from 'checkout/backend';
import { ModalLoader } from './modal-loader';
@ -22,13 +33,13 @@ export interface ModalContainerProps {
setModalFromEvents: (events: Event[]) => any;
acceptModel: () => any;
pollInvoiceEvents: (capiEndpoint: string, accessToken: string, events: Event[]) => any;
setErrorFormInfo: () => any;
goToFormInfo: (formInfo: FormInfo) => any;
acceptError: () => any;
setModalInteractionPollingStatus: (status: boolean) => any;
}
const isInteractionPolling = (modal: ModalState) =>
(modal.name === ModalName.modalInteraction && (modal as ModalInteraction).pollingEvents);
modal.name === ModalName.modalInteraction && (modal as ModalInteraction).pollingEvents;
class ModalContainerDef extends React.Component<ModalContainerProps> {
@ -49,7 +60,7 @@ class ModalContainerDef extends React.Component<ModalContainerProps> {
props.acceptModel();
}
if (props.unhandledError) {
props.setErrorFormInfo();
props.goToFormInfo(new ResultFormInfo(ResultType.error));
props.acceptError();
}
}
@ -104,7 +115,7 @@ const mapDispatchToProps = (dispatch: Dispatch<any>) => ({
setModalFromEvents: bindActionCreators(setModalFromEvents, dispatch),
acceptModel: bindActionCreators(accept, dispatch),
pollInvoiceEvents: bindActionCreators(pollInvoiceEvents, dispatch),
setErrorFormInfo: bindActionCreators(setErrorFormInfo, dispatch),
goToFormInfo: bindActionCreators(goToFormInfo, dispatch),
acceptError: bindActionCreators(acceptError, dispatch),
setModalInteractionPollingStatus: bindActionCreators(setModalInteractionPollingStatus, dispatch)
});

View File

@ -0,0 +1,8 @@
import { values } from 'lodash';
import { FieldsConfig, ItemConfig } from './fields-config';
export const calcFormHeight = (defaultHeight: number, fieldsConfig: FieldsConfig): number => {
const count = values(fieldsConfig)
.reduce((acc: number, current: ItemConfig) => current.visible ? ++acc : acc, 0);
return defaultHeight + count * 52;
};

View File

@ -1,14 +1,21 @@
import { CardFormInfo, CardFormState, CardFormValues, ConfigState, FormName, ModelState } from 'checkout/state';
import { Locale } from 'checkout/locale';
import {
CardFormInfo,
CardFormValues,
ConfigState,
ModelState,
} from 'checkout/state';
import {Locale} from 'checkout/locale';
import { FieldsConfig } from '../fields-config';
export interface CardFormProps {
locale: Locale;
config: ConfigState;
model: ModelState;
cardFormInfo: CardFormInfo;
cardForm: CardFormState;
locale: Locale;
formValues: CardFormValues;
fieldsConfig: FieldsConfig;
pay: (c: ConfigState, m: ModelState, v: CardFormValues) => any;
setViewInfoError: (hasError: boolean, formName: FormName) => any;
setViewInfoError: (hasError: boolean) => any;
prepareToPay: () => any;
setViewInfoHeight: (height: number) => any;
}

View File

@ -1,3 +0,0 @@
.pay_button {
margin-top: 20px;
}

View File

@ -1,3 +0,0 @@
export const pay_button: string;
export const form: string;
export const _withAmount: string;

View File

@ -1,18 +1,27 @@
import { connect } from 'react-redux';
import * as React from 'react';
import { InjectedFormProps, reduxForm } from 'redux-form';
import { get } from 'lodash';
import * as styles from './card-form.scss';
import * as formStyles from '../form-container.scss';
import * as commonFormStyles from 'checkout/styles/forms.scss';
import { CardFormProps } from './card-form-props';
import { Button } from '../button';
import { Amount, CardHolder, CardNumber, Email, ExpireDate, SecureCode } from './fields';
import { CardFormValues, FormName, ModalForms, ModalName, ModalState, PaymentStatus, State } from 'checkout/state';
import { getAmount } from '../../amount-resolver';
import { findNamed, formatAmount } from 'checkout/utils';
import { bindActionCreators, Dispatch } from 'redux';
import { pay, prepareToPay, setViewInfoError } from 'checkout/actions';
import { get } from 'lodash';
import * as formStyles from 'checkout/styles/forms.scss';
import { CardFormProps } from './card-form-props';
import { CardHolder, CardNumber, ExpireDate, SecureCode } from './fields';
import {
CardFormValues,
FormName,
ModalForms,
ModalName,
ModalState,
PaymentStatus,
State
} from 'checkout/state';
import { findNamed } from 'checkout/utils';
import { payCardData, prepareToPay, setViewInfoError, setViewInfoHeight } from 'checkout/actions';
import { PayButton } from '../pay-button';
import { Header } from '../header/header';
import { calcFormHeight } from '../calc-form-height';
import { toFieldsConfig } from '../fields-config';
import { Email, Amount } from '../common-fields';
const toCardFormInfo = (modals: ModalState[]) => {
const info = (findNamed(modals, ModalName.modalForms) as ModalForms).formsInfo;
@ -23,26 +32,20 @@ const mapStateToProps = (state: State) => ({
cardFormInfo: toCardFormInfo(state.modals),
config: state.config,
model: state.model,
cardForm: state.form.cardForm,
formValues: get(state.form, 'cardForm.values'),
locale: state.config.locale,
formValues: get(state.form, 'cardForm.values')
fieldsConfig: toFieldsConfig(state.config.initConfig, state.model.invoiceTemplate)
});
const mapDispatchToProps = (dispatch: Dispatch<any>) => ({
pay: bindActionCreators(pay, dispatch),
pay: bindActionCreators(payCardData, dispatch),
setViewInfoError: bindActionCreators(setViewInfoError, dispatch),
prepareToPay: bindActionCreators(prepareToPay, dispatch)
prepareToPay: bindActionCreators(prepareToPay, dispatch),
setViewInfoHeight: bindActionCreators(setViewInfoHeight, dispatch)
});
type Props = InjectedFormProps & CardFormProps;
const PayButton: React.SFC<CardFormProps> = (props) => {
const amount = formatAmount(getAmount(props.config.initConfig.integrationType, props.model));
const label = amount ? `${amount.value} ${amount.symbol}` : null;
return <Button className={styles.pay_button} type='submit'
style='primary' id='pay-btn'>{props.locale['form.button.pay.label']} {label}</Button>;
};
class CardFormDef extends React.Component<Props> {
constructor(props: Props) {
@ -50,64 +53,75 @@ class CardFormDef extends React.Component<Props> {
this.submit = this.submit.bind(this);
}
submit(values: CardFormValues) {
const activeElement = document.activeElement as HTMLInputElement;
activeElement.blur();
this.props.prepareToPay();
pay(values: CardFormValues) {
const {config, model} = this.props;
this.props.prepareToPay();
this.props.pay(config, model, values);
}
init(values: CardFormValues) {
this.props.initialize({
email: get(values, 'email'),
amount: get(values, 'amount')
});
}
submit(values: CardFormValues) {
(document.activeElement as HTMLElement).blur();
this.pay(values);
}
componentWillMount() {
switch (this.props.cardFormInfo.paymentStatus) {
const {cardFormInfo: {paymentStatus}, formValues} = this.props;
this.props.setViewInfoError(false);
switch (paymentStatus) {
case PaymentStatus.pristine:
this.props.reset();
this.init(formValues);
break;
case PaymentStatus.needRetry:
this.props.prepareToPay();
const {config, model, formValues} = this.props;
this.props.pay(config, model, formValues);
this.pay(formValues);
break;
}
}
componentDidMount() {
this.props.setViewInfoHeight(calcFormHeight(288, this.props.fieldsConfig));
}
componentWillReceiveProps(props: Props) {
if (props.submitFailed) {
this.props.setViewInfoError(true, FormName.cardForm);
props.setViewInfoError(true);
}
}
render() {
const locale = this.props.locale;
const {fieldsConfig} = this.props.cardFormInfo;
const {handleSubmit, fieldsConfig: {email, amount}, locale} = this.props;
return (
<form onSubmit={this.props.handleSubmit(this.submit)} className={styles.form}>
<div className={formStyles.header}>
{/*{hasBack(this.props.formsFlow) ? <ChevronBack/> : null}*/}
<div className={formStyles.title}>
{locale['form.header.pay.card.label']}
<form onSubmit={handleSubmit(this.submit)}>
<div>
<Header title={locale['form.header.pay.card.label']}/>
<div className={formStyles.formGroup}>
<CardNumber/>
</div>
<div className={formStyles.formGroup}>
<ExpireDate/>
<SecureCode/>
</div>
<div className={formStyles.formGroup}>
<CardHolder/>
</div>
{email.visible ?
<div className={formStyles.formGroup}>
<Email/>
</div> : false
}
{amount.visible ?
<div className={formStyles.formGroup}>
<Amount cost={amount.cost}/>
</div> : false
}
</div>
<div className={commonFormStyles.formGroup}>
<CardNumber/>
</div>
<div className={commonFormStyles.formGroup}>
<ExpireDate/>
<SecureCode/>
</div>
<div className={commonFormStyles.formGroup}>
<CardHolder/>
</div>
{fieldsConfig.email.visible ?
<div className={commonFormStyles.formGroup}>
<Email/>
</div> : false
}
{fieldsConfig.amount.visible ?
<div className={commonFormStyles.formGroup}>
<Amount/>
</div> : false
}
<PayButton {...this.props}/>
<PayButton/>
</form>
);
}

View File

@ -1,15 +0,0 @@
import { CostType, InvoiceTemplateLineCostRange } from 'checkout/backend';
import { toNumber } from 'lodash';
import { validateAmount } from '../validation';
import { AmountProps } from './amount';
export const validate = (value: string, props: AmountProps): boolean => {
const binded = validateAmount.bind(null, toNumber(value) * 100);
switch (props.cost.costType) {
case CostType.InvoiceTemplateLineCostRange:
const range = (props.cost as InvoiceTemplateLineCostRange).range;
return binded(range.lowerBound, range.upperBound);
case CostType.InvoiceTemplateLineCostUnlim:
return binded();
}
};

View File

@ -7,7 +7,7 @@ import { Input } from '../../../input';
import { cardHolderFormatter } from '../format';
import { validateCardHolder } from '../validation';
import { Locale } from 'checkout/locale';
import { isError } from '../error-predicate';
import { isError } from '../../../common-fields/error-predicate';
type FieldProps = WrappedFieldInputProps & WrappedFieldProps;

View File

@ -5,7 +5,7 @@ import * as styles from './card-number.scss';
import { cardNumberFormatter } from '../format/index';
import { Locale } from 'src/locale/locale';
import { State } from 'checkout/state';
import { isError } from '../error-predicate';
import { isError } from '../../../common-fields/error-predicate';
import { IconType, Input } from 'checkout/components';
type FieldProps = WrappedFieldInputProps & WrappedFieldProps;

View File

@ -7,7 +7,7 @@ import { Input } from '../../../input';
import { expireDateFormatter } from '../format';
import { validateExpireDate } from '../validation';
import { Locale } from 'checkout/locale';
import { isError } from '../error-predicate';
import { isError } from '../../../common-fields/error-predicate';
type FieldProps = WrappedFieldInputProps & WrappedFieldProps;

View File

@ -1,6 +1,4 @@
export * from './card-holder';
export * from './card-number';
export * from './email';
export * from './expire-date';
export * from './secure-code';
export * from './amount';

View File

@ -7,7 +7,7 @@ import { secureCodeFormatter } from '../format';
import { validateSecureCode } from '../validation';
import { State } from 'checkout/state';
import { Locale } from 'checkout/locale';
import { isError } from '../error-predicate';
import { isError } from '../../../common-fields/error-predicate';
type FieldProps = WrappedFieldInputProps & WrappedFieldProps;

View File

@ -1,6 +1,4 @@
export * from './card-number';
export * from './card-holder';
export * from './email';
export * from './expire-date';
export * from './secure-code';
export * from './amount';

View File

@ -1,19 +1,13 @@
import * as React from 'react';
import {Icon, IconType} from 'checkout/components';
class ChevronBackDef extends React.Component<any> {
constructor(props: any) {
super(props);
}
render(): any {
// return (
// <div className={formStyles.back_btn} onClick={this.back}>
// <Icon icon={IconType.chevronLeft}/>
// </div>
// );
return null;
}
interface ChevronBackProps {
className: string;
back: () => any;
}
export const ChevronBack = ChevronBackDef;
export const ChevronBack: React.SFC<ChevronBackProps> = (props) => (
<div className={props.className} onClick={props.back}>
<Icon icon={IconType.chevronLeft}/>
</div>
);

View File

@ -2,19 +2,28 @@ import * as React from 'react';
import { connect } from 'react-redux';
import { Field, WrappedFieldInputProps, WrappedFieldProps } from 'redux-form';
import { IconType, Input } from 'checkout/components';
import { CardFormInfo, FormName, ModalForms, ModalName, ModalState, State } from 'checkout/state';
import { InvoiceTemplateLineCostRange, InvoiceTemplateLineCostUnlim } from 'checkout/backend';
import { getPlaceholder } from './get-placeholder';
import { validate } from './validate';
import { isError } from '../error-predicate';
import { Locale } from 'checkout/locale';
import { InvoiceTemplateLineCostRange, InvoiceTemplateLineCostUnlim } from 'checkout/backend';
import { State } from 'checkout/state';
type FieldProps = WrappedFieldInputProps & WrappedFieldProps;
interface OwnProps {
cost: InvoiceTemplateLineCostRange | InvoiceTemplateLineCostUnlim;
}
export interface AmountProps {
cost: InvoiceTemplateLineCostRange | InvoiceTemplateLineCostUnlim;
locale: Locale;
}
type FieldProps = WrappedFieldInputProps & WrappedFieldProps;
const mapStateToProps = (state: State, ownProps: OwnProps) => ({
cost: ownProps.cost,
locale: state.config.locale
});
const CustomInput: React.SFC<FieldProps & AmountProps> = (props) => (
<Input
@ -33,20 +42,8 @@ const AmountDef: React.SFC<AmountProps> = (props) => (
<Field
name='amount'
component={(fieldProps: FieldProps) => CustomInput({...fieldProps, ...props})}
validate={(value) => validate(value, props)}
validate={(value) => validate(value, props.cost)}
/>
);
const toCost = (s: ModalState[]) => {
const modalForms = s.find((modal) => modal.name === ModalName.modalForms) as ModalForms;
const cardFormInfo = modalForms.formsInfo.find((info) => info.name === FormName.cardForm) as CardFormInfo;
return cardFormInfo.fieldsConfig.amount.cost;
};
const mapStateToProps = (state: State) => ({
cost: toCost(state.modals),
locale: state.config.locale
}
);
export const Amount = connect(mapStateToProps)(AmountDef);

View File

@ -0,0 +1,14 @@
import { CostType, InvoiceTemplateLineCostRange, InvoiceTemplateLineCostUnlim } from 'checkout/backend';
import { toNumber } from 'lodash';
import { validateAmount } from '../validation/amount';
export const validate = (value: string, cost: InvoiceTemplateLineCostRange | InvoiceTemplateLineCostUnlim): boolean => {
const binded = validateAmount.bind(null, toNumber(value) * 100);
switch (cost.costType) {
case CostType.InvoiceTemplateLineCostRange:
const range = (cost as InvoiceTemplateLineCostRange).range;
return binded(range.lowerBound, range.upperBound);
case CostType.InvoiceTemplateLineCostUnlim:
return binded();
}
};

View File

@ -3,10 +3,10 @@ import { connect } from 'react-redux';
import { Field, WrappedFieldInputProps, WrappedFieldProps } from 'redux-form';
import { IconType } from 'checkout/components/ui';
import { State } from 'checkout/state';
import { Input } from '../../../input';
import { validateEmail } from '../validation';
import { Input } from '../../input';
import { Locale } from 'checkout/locale';
import { isError } from '../error-predicate';
import { validateEmail } from '../validation/email';
type FieldProps = WrappedFieldInputProps & WrappedFieldProps;

View File

@ -0,0 +1 @@
export * from './phone/format-phone-number';

View File

@ -0,0 +1,19 @@
import * as libphonenumber from 'libphonenumber-js';
const format = (e: KeyboardEvent) => {
const target = e.currentTarget as HTMLInputElement;
const value = target.value;
if (value.slice(0, 2) === '+7') {
target.value = new libphonenumber.AsYouType('RU').input(value);
} else {
target.value = `+${libphonenumber.getPhoneCode('RU')} `;
}
};
export function phoneNumberFormatter(element: Element) {
element.addEventListener('focus', format);
element.addEventListener('keypress', format);
element.addEventListener('keydown', format);
element.addEventListener('change', format);
element.addEventListener('input', format);
}

View File

@ -0,0 +1,3 @@
export * from './email';
export * from './amount';
export * from './phone';

View File

@ -0,0 +1 @@
export * from './phone';

View File

@ -0,0 +1,44 @@
import * as React from 'react';
import { connect } from 'react-redux';
import { Field, WrappedFieldInputProps, WrappedFieldProps } from 'redux-form';
import { IconType } from 'checkout/components/ui';
import { State } from 'checkout/state';
import { Input } from '../../input';
import { Locale } from 'checkout/locale';
import { isError } from '../error-predicate';
import { validatePhone } from '../validation/phone';
import { phoneNumberFormatter } from '../format';
type FieldProps = WrappedFieldInputProps & WrappedFieldProps;
export interface PhoneDefProps {
locale: Locale;
}
const mapStateToProps = (state: State) => ({
locale: state.config.locale
});
const CustomInput: React.SFC<FieldProps & PhoneDefProps> = (props) => (
<Input
{...props.input}
{...props.meta}
error={isError(props.meta)}
formatter={phoneNumberFormatter}
icon={IconType.phone}
placeholder={props.locale['form.input.phone.placeholder']}
mark={true}
type='tel'
id='phone-input'
/>
);
export const PhoneDef: React.SFC<PhoneDefProps> = (props) => (
<Field
name='phone'
component={(fieldProps: FieldProps) => CustomInput({...fieldProps, ...props})}
validate={validatePhone}
/>
);
export const Phone = connect(mapStateToProps)(PhoneDef);

View File

@ -0,0 +1,8 @@
import * as libphonenumber from 'libphonenumber-js';
export function validatePhone(value: string): boolean {
if (!value) {
return true;
}
return !libphonenumber.isValidNumber(value, 'RU');
}

View File

@ -0,0 +1,18 @@
import { InvoiceTemplateLineCostRange, InvoiceTemplateLineCostUnlim } from 'checkout/backend';
export interface ItemConfig {
visible: boolean;
}
export interface AmountConfig extends ItemConfig {
cost?: InvoiceTemplateLineCostRange | InvoiceTemplateLineCostUnlim;
}
export interface EmailConfig extends ItemConfig {
value?: string;
}
export interface FieldsConfig {
amount: AmountConfig;
email: EmailConfig;
}

View File

@ -0,0 +1,2 @@
export * from './fields-config';
export * from './to-fields-config';

View File

@ -1,18 +1,11 @@
import { values } from 'lodash';
import {
InvoiceTemplate,
InvoiceTemplateLineCostRange,
InvoiceTemplateLineCostUnlim,
InvoiceTemplateSingleLine
} from 'checkout/backend';
import {
FieldsConfig,
CardFormInfo,
ItemConfig,
AmountConfig,
EmailConfig
} from 'checkout/state';
import { InitConfig, IntegrationType } from 'checkout/config';
import { AmountConfig, EmailConfig, FieldsConfig } from './fields-config';
const toSingleLineAmountConfig = (c: InvoiceTemplateSingleLine): AmountConfig => {
const result = {visible: false} as AmountConfig;
@ -51,18 +44,7 @@ const toEmailConfig = (email: string): EmailConfig => {
: {visible: true};
};
const toFieldsConfig = (c: InitConfig, t: InvoiceTemplate): FieldsConfig => ({
export const toFieldsConfig = (c: InitConfig, t: InvoiceTemplate): FieldsConfig => ({
amount: toAmountConfig(c.integrationType, t),
email: toEmailConfig(c.email)
});
const calcHeight = (fieldsConfig: FieldsConfig): number => {
const count = values(fieldsConfig)
.reduce((acc: number, current: ItemConfig) => current.visible ? ++acc : acc, 0);
return 288 + count * 52;
};
export const toCardFormInfo = (c: InitConfig, t: InvoiceTemplate): CardFormInfo => {
const fieldConfig = toFieldsConfig(c, t);
return new CardFormInfo(calcHeight(fieldConfig), fieldConfig, true);
};

View File

@ -1,5 +1,7 @@
import { FormInfo } from 'checkout/state/modal/form-info';
import { FormViewInfo } from 'checkout/state';
export interface FormContainerProps {
activeFormInfo: FormInfo;
viewInfo: FormViewInfo;
}

View File

@ -18,11 +18,11 @@
padding: 30px 20px;
position: relative;
overflow: hidden;
transition: height .4s;
@include responsive(sm) {
padding: 30px;
min-height: auto;
transition: height .75s;
}
&._error {
@ -30,24 +30,46 @@
}
}
.animationFormContainer {
height: 100%;
position: relative;
form {
height: 100%;
width: 100%;
display: flex;
flex-wrap: nowrap;
flex-direction: column;
justify-content: space-between;
}
}
.header {
margin-bottom: 20px;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
position: relative;
.back_btn {
display: none;
@include responsive(sm) {
display: flex;
}
}
}
.back_btn {
display: none;
height: 20px;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
cursor: pointer;
@include responsive(sm) {
display: flex;
}
position: absolute;
width: 20px;
svg {
height: 15px;
width: 9px;
@ -68,14 +90,3 @@
}
}
.appearLoader {
animation: fadein .5s;
}
.enterLoader {
animation: fadein .5s;
}
.leaveLoader {
animation: fadeout .25s;
}

View File

@ -1,18 +1,12 @@
export const container: string;
export const form: string;
export const _error: string;
export const shake: string;
export const header: string;
export const back_btn: string;
export const title: string;
export const growth: string;
export const popup: string;
export const appearForm: string;
export const enterForm: string;
export const leaveForm: string;
export const appearLoader: string;
export const enterLoader: string;
export const leaveLoader: string;
export const _cardForm: string;
export const _resultForm: string;
export const _cardFormAmount: string;
export const animationFormContainer: string;

View File

@ -4,59 +4,47 @@ import * as CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
import * as cx from 'classnames';
import * as styles from './form-container.scss';
import { CardForm } from './card-form';
import { FormName, ModalForms, ModalName, ModalState, State } from 'checkout/state';
import { FormName, ModalForms, ModalName, State } from 'checkout/state';
import { PaymentMethods } from './payment-methods';
import { FormContainerProps } from './form-container-props';
import { FormLoader } from './form-loader';
import { ResultForm } from './result-form';
import { findNamed } from 'checkout/utils';
import { WalletForm } from './wallet-form';
const toActiveFormInfo = (modals: ModalState[]) => {
const info = (findNamed(modals, ModalName.modalForms) as ModalForms).formsInfo;
return info.find((item) => item.active);
const mapStateToProps = (state: State) => {
const modalForms = (findNamed(state.modals, ModalName.modalForms) as ModalForms);
return {
activeFormInfo: modalForms.formsInfo.find((item) => item.active),
viewInfo: modalForms.viewInfo
};
};
const mapStateToProps = (state: State) => ({
activeFormInfo: toActiveFormInfo(state.modals)
});
class FormContainerDef extends React.Component<FormContainerProps> {
const FormContainerDef: React.SFC<FormContainerProps> = (props) => {
const {name, viewInfo} = props.activeFormInfo;
return (
<div className={styles.container}>
<div className={cx(styles.form, {
[styles._error]: viewInfo.error
})}
style={{height: viewInfo.height}}>
<CSSTransitionGroup
component='div'
transitionName={viewInfo.slideDirection}
transitionEnterTimeout={550}
transitionLeaveTimeout={550}
>
{name === FormName.paymentMethods ? <PaymentMethods/> : null}
{name === FormName.cardForm ? <CardForm/> : null}
{name === FormName.resultForm ? <ResultForm/> : null}
</CSSTransitionGroup>
<CSSTransitionGroup
component='div'
transitionName={{
appear: styles.appearLoader,
enter: styles.enterLoader,
leave: styles.leaveLoader
}}
transitionLeaveTimeout={200}
transitionEnterTimeout={450}
transitionAppearTimeout={450}
transitionAppear={true}
transitionEnter={true}
transitionLeave={true}
>
render() {
const {activeFormInfo: {name}, viewInfo} = this.props;
return (
<div className={styles.container}>
<div
className={cx(styles.form, {[styles._error]: viewInfo.error})}
style={{height: viewInfo.height}}>
<CSSTransitionGroup
component='div'
className={styles.animationFormContainer}
transitionName={viewInfo.slideDirection}
transitionEnterTimeout={550}
transitionLeaveTimeout={550}>
{name === FormName.paymentMethods ? <PaymentMethods/> : null}
{name === FormName.cardForm ? <CardForm/> : null}
{name === FormName.walletForm ? <WalletForm/> : null}
{name === FormName.resultForm ? <ResultForm/> : null}
</CSSTransitionGroup>
{viewInfo.inProcess ? <FormLoader/> : null}
</CSSTransitionGroup>
</div>
</div>
</div>
);
};
);
}
}
export const FormContainer = connect(mapStateToProps)(FormContainerDef);

View File

@ -14,3 +14,11 @@
justify-content: center;
background: rgba(255, 255, 255, .9);
}
.appear {
animation: fadein .5s;
}
.leave {
animation: fadeout .2s;
}

View File

@ -1 +1,3 @@
export const loader: string;
export const appear: string;
export const leave: string;

View File

@ -1,9 +1,17 @@
import * as React from 'react';
import * as styles from './form-loader.scss';
import { CSSTransitionGroup } from 'react-transition-group';
import { appear, leave, loader } from './form-loader.scss';
import { Loader } from 'checkout/components';
export const FormLoader: React.SFC = () => (
<div className={styles.loader} id='form-loader'>
<Loader/>
</div>
<CSSTransitionGroup
transitionName={{enter: null, appear, leave}}
transitionEnter={false}
transitionAppear={true}
transitionAppearTimeout={500}
transitionLeaveTimeout={200}>
<div key='form-loader' className={loader} id='form-loader'>
<Loader/>
</div>
</CSSTransitionGroup>
);

View File

@ -0,0 +1,43 @@
import { bindActionCreators, Dispatch } from 'redux';
import * as React from 'react';
import { connect } from 'react-redux';
import * as formStyles from '../form-container.scss';
import { Direction, goToFormInfo } from 'checkout/actions';
import { findInfoWithPrevious, findNamed } from 'checkout/utils';
import { FormInfo, ModalForms, ModalName, ModalState, State } from 'checkout/state';
import { ChevronBack } from '../chevron-back';
export interface HeaderProps {
title: string;
goToFormInfo: (formInfo: FormInfo, direction: Direction) => any;
destination: FormInfo;
}
const getDestination = (modals: ModalState[]): FormInfo => {
const modalForms = findNamed(modals, ModalName.modalForms) as ModalForms;
const withPrevious = findInfoWithPrevious(modals);
return withPrevious ? findNamed(modalForms.formsInfo, withPrevious.previous) as FormInfo : null;
};
const mapStateToProps = (state: State) => ({
destination: getDestination(state.modals)
});
const mapDispatchToProps = (dispatch: Dispatch<any>) => ({
goToFormInfo: bindActionCreators(goToFormInfo, dispatch)
});
const HeaderDef: React.SFC<HeaderProps> = (props) => (
<div className={formStyles.header}>
{props.destination ?
<ChevronBack
className={formStyles.back_btn}
back={props.goToFormInfo.bind(null, props.destination, Direction.back)}/> : null
}
<div className={formStyles.title}>
{props.title}
</div>
</div>
);
export const Header = connect(mapStateToProps, mapDispatchToProps)(HeaderDef);

View File

@ -0,0 +1 @@
export * from './header';

View File

@ -1,2 +1,3 @@
export * from './form-container';
export * from './form-container-props';
export * from './common-fields';

View File

@ -0,0 +1 @@
export * from './pay-button';

View File

@ -0,0 +1,34 @@
import * as React from 'react';
import { connect } from 'react-redux';
import { ModelState, State } from 'checkout/state';
import { IntegrationType } from 'checkout/config';
import { getAmount } from '../../amount-resolver';
import { formatAmount } from 'checkout/utils';
import { Button } from 'checkout/components';
import { Locale } from 'checkout/locale';
export interface PayButtonProps {
locale: Locale;
label: string;
}
const PayButtonDef: React.SFC<PayButtonProps> = (props) => (
<Button
type='submit'
style='primary'
id='pay-btn'>
{props.locale['form.button.pay.label']} {props.label}
</Button>
);
const toLabel = (integrationType: IntegrationType, model: ModelState): string => {
const amount = formatAmount(getAmount(integrationType, model));
return amount ? `${amount.value} ${amount.symbol}` : null;
};
const mapStateToProps = (state: State) => ({
locale: state.config.locale,
label: toLabel(state.config.initConfig.integrationType, state.model)
});
export const PayButton = connect(mapStateToProps)(PayButtonDef);

View File

@ -0,0 +1,22 @@
import * as React from 'react';
import * as styles from '../payment-methods.scss';
import { Locale } from 'checkout/locale';
import { CardFormInfo, FormInfo, FormName } from 'checkout/state';
import { BankCardIcon } from './icons/bank-card-icon';
interface BankCardProps {
locale: Locale;
setFormInfo: (formInfo: FormInfo) => any;
}
const toBankCard = (props: BankCardProps) => props.setFormInfo(new CardFormInfo(FormName.paymentMethods));
export const BankCard: React.SFC<BankCardProps> = (props) => (
<li className={styles.method} onClick={toBankCard.bind(null, props)}>
<BankCardIcon/>
<div className={styles.title}>
{props.locale['form.payment.method.name.card.label']}
<hr/>
</div>
</li>
);

View File

@ -0,0 +1,31 @@
import * as React from 'react';
import * as styles from '../../payment-methods.scss';
export const BankCardIcon: React.SFC = () => (
<div className={styles.icon}>
{/* tslint:disable:max-line-length */}
<svg width='40px' height='40px' viewBox='0 0 40 40'>
<g stroke='none' strokeWidth='1' fill='none' fillRule='evenodd'>
<g transform='translate(-45.000000, -366.000000)'>
<g transform='translate(5.000000, 276.000000)'>
<g transform='translate(20.000000, 70.000000)'>
<g transform='translate(20.000000, 20.000000)'>
<rect fill='#FFFFFF' x='0' y='0' width='40' height='40'/>
<path
d='M29.99436,25 L5.0068,25 C3.899,25 3,24.0712 3,22.9252 L3,9.0748 C3,7.9288 3.899,7 5.0068,7 L29.99436,7 C31.10216,7 32,7.9288 32,9.0748 L32,22.9252 C32,24.0712 31.10216,25 29.99436,25 Z'
stroke='#0077FF' strokeWidth='2' fill='#FFFFFF'/>
<path
d='M34.99436,34 L10.0068,34 C8.899,34 8,33.0712 8,31.9252 L8,18.0748 C8,16.9288 8.899,16 10.0068,16 L34.99436,16 C36.10216,16 37,16.9288 37,18.0748 L37,31.9252 C37,33.0712 36.10216,34 34.99436,34 Z'
stroke='#0077FF' strokeWidth='2' fill='#FFFFFF'/>
<polygon fill='#0077FF' points='4 13 31 13 31 12 4 12'/>
<polygon fill='#0077FF' points='11 28 27 28 27 27 11 27'/>
<polygon fill='#0077FF' points='30 28 34 28 34 27 30 27'/>
</g>
</g>
</g>
</g>
</g>
</svg>
{/* tslint:enable:max-line-length */}
</div>
);

View File

@ -0,0 +1,31 @@
import * as React from 'react';
import * as styles from '../../payment-methods.scss';
export const TerminalsIcon: React.SFC = () => (
<div className={styles.icon}>
{/* tslint:disable:max-line-length */}
<svg width='40px' height='40px' viewBox='0 0 40 40'>
<g stroke='none' strokeWidth='1' fill='none' fillRule='evenodd'>
<g transform='translate(-45.000000, -456.000000)'>
<g transform='translate(5.000000, 276.000000)'>
<g transform='translate(20.000000, 160.000000)'>
<g transform='translate(20.000000, 20.000000)'>
<rect fill='#FFFFFF' x='0' y='0' width='40' height='40'/>
<g transform='translate(8.000000, 3.000000)'>
<path
d='M24,33 L0,33 L0,2.3674 C0,1.0602 1.05726316,0 2.36084211,0 L21.6391579,0 C22.9427368,0 24,1.0602 24,2.3674 L24,33 Z'
stroke='#0077FF' strokeWidth='2' fill='#FFFFFF'/>
<path d='M3,16 L21,16 L21,3 L3,3 L3,16 Z M4,15 L20,15 L20,4 L4,4 L4,15 Z'
fill='#0077FF'/>
<polygon fill='#0077FF' points='0 19 24 19 24 18 0 18'/>
<polygon fill='#0077FF' points='15 24 21 24 21 23 15 23'/>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
{/* tslint:enable:max-line-length */}
</div>
);

View File

@ -0,0 +1,43 @@
import * as React from 'react';
import * as styles from '../../payment-methods.scss';
export const WalletsIcon: React.SFC = () => (
<div className={styles.icon}>
{/* tslint:disable:max-line-length */}
<svg width='40px' height='40px' viewBox='0 0 40 40' version='1.1'>
<g stroke='none' strokeWidth='1' fill='none' fillRule='evenodd'>
<g transform='translate(-700.000000, -384.000000)'>
<g transform='translate(675.000000, 170.000000)'>
<g transform='translate(0.000000, 194.000000)'>
<g transform='translate(25.000000, 20.000000)'>
<rect fill='#FFFFFF' x='0' y='0' width='40' height='40'/>
<g strokeWidth='1' transform='translate(5.000000, 6.000000)'>
<g transform='translate(0.000000, 2.000000)'>
<g transform='translate(1.000000, 0.000000)' fill='#FFFFFF'>
<path
d='M29,9.56521739 L29,23.7155652 C29,24.9775217 27.98964,26 26.74264,26 L2.25736,26 C1.01036,26 0,24.9775217 0,23.7155652 L0,0.138695652'
id='Fill-1'/>
</g>
<path
d='M30,9.56521739 L30,23.7155652 C30,24.9775217 28.98964,26 27.74264,26 L2.25736,26 C1.01036,26 0,24.9775217 0,23.7155652 L0,0.138695652'
id='Stroke-3' stroke='#0077FF' strokeWidth='2'/>
</g>
<path
d='M30,12 L30,9.5784 C30,7.602 28.4514,6 27.68,6 L3.42432,6 C1.02312,6 0,4.9416 0,3.636 L0,2.364 C0,1.0584 1.02312,0 2.2852,0 L21.83128,0 C23.76848,0 25.33912,1.6248 25.33912,3.6288 L25.33912,6'
id='Stroke-5' stroke='#0077FF' strokeWidth='2'/>
<g transform='translate(7.000000, 13.000000)'>
<polyline fill='#FFFFFF'
points='0.566666667 7.875 8.5 0 8.5 9 16.4333333 1.125'/>
<polyline stroke='#0077FF'
points='0.566666667 7.875 8.5 0 8.5 9 16.4333333 1.125'/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
{/* tslint:enable:max-line-length */}
</div>
);

View File

@ -0,0 +1,3 @@
export * from './bank-card';
export * from './wallets';
export * from './terminals';

View File

@ -0,0 +1,23 @@
import * as React from 'react';
import * as styles from '../payment-methods.scss';
import {Locale} from 'checkout/locale';
import {TerminalsIcon} from './icons/terminals-icon';
interface TerminalsProps {
locale: Locale;
}
export const Terminals: React.SFC<TerminalsProps> = (props) => (
<li className={styles.method}>
<TerminalsIcon />
<div className={styles.text}>
<h5 className={styles.title}>
{props.locale['form.payment.method.name.cash.label']}
<hr/>
</h5>
<p className={styles.description}>
{props.locale['form.payment.method.description.euroset.text']}
</p>
</div>
</li>
);

View File

@ -0,0 +1,27 @@
import * as React from 'react';
import * as styles from '../payment-methods.scss';
import {Locale} from 'checkout/locale';
import {WalletsIcon} from './icons/wallets-icon';
import { FormInfo, FormName, WalletFormInfo } from 'checkout/state';
interface WalletsProps {
locale: Locale;
setFormInfo: (formInfo: FormInfo) => any;
}
const toWallets = (props: WalletsProps) => props.setFormInfo(new WalletFormInfo(FormName.paymentMethods));
export const Wallets: React.SFC<WalletsProps> = (props) => (
<li className={styles.method} onClick={toWallets.bind(null, props)}>
<WalletsIcon />
<div className={styles.text}>
<h5 className={styles.title}>
{props.locale['form.payment.method.name.wallet.label']}
<hr/>
</h5>
<p className={styles.description}>
{props.locale['form.payment.method.description.qiwi.text']}
</p>
</div>
</li>
);

View File

@ -53,9 +53,8 @@
display: inline-table;
hr {
border-color: $light-blue;
opacity: .4;
border-bottom: 0;
border: 1px solid $lightest-blue2;
border-bottom-width: 0;
padding: 0;
margin: 0 0 3px;
}

Some files were not shown because too many files have changed in this diff Show More