Payment Modal: Various fixes (#2124)

This commit is contained in:
Alexander Zinchuk 2022-11-10 18:28:05 +04:00
parent f272f63afc
commit d10df1a705
12 changed files with 59 additions and 35 deletions

View File

@ -166,6 +166,7 @@ export interface ApiPoll {
export type ApiInputInvoice = { export type ApiInputInvoice = {
chatId: string; chatId: string;
messageId: number; messageId: number;
isExtendedMedia?: boolean;
} | { } | {
slug: string; slug: string;
}; };

View File

@ -1,6 +1,6 @@
const VISA = /^4[0-9]{12}(?:[0-9]{1,3})?$/; const VISA = /^4\d/;
const MASTERCARD1 = /^5[1-5][0-9]{11,14}$/; const MASTERCARD1 = /^5[1-5]/;
const MASTERCARD2 = /^2[2-7][0-9]{11,14}$/; const MASTERCARD2 = /^2[2-7]\d{2}/;
const MIR = /^220[0-4]/; const MIR = /^220[0-4]/;
export enum CardType { export enum CardType {

View File

@ -1,5 +1,5 @@
import type { FC } from '../../../lib/teact/teact'; import type { FC } from '../../../lib/teact/teact';
import React, { memo, useCallback, useEffect } from '../../../lib/teact/teact'; import React, { memo } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { ApiMessage } from '../../../api/types'; import type { ApiMessage } from '../../../api/types';
@ -7,7 +7,6 @@ import type { ApiMessage } from '../../../api/types';
import { IS_TOUCH_ENV } from '../../../util/environment'; import { IS_TOUCH_ENV } from '../../../util/environment';
import { selectChatMessage, selectCurrentMessageList } from '../../../global/selectors'; import { selectChatMessage, selectCurrentMessageList } from '../../../global/selectors';
import useMouseInside from '../../../hooks/useMouseInside'; import useMouseInside from '../../../hooks/useMouseInside';
import useFlag from '../../../hooks/useFlag';
import Menu from '../../ui/Menu'; import Menu from '../../ui/Menu';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
@ -31,16 +30,6 @@ const BotKeyboardMenu: FC<OwnProps & StateProps> = ({
const [handleMouseEnter, handleMouseLeave] = useMouseInside(isOpen, onClose); const [handleMouseEnter, handleMouseLeave] = useMouseInside(isOpen, onClose);
const { isKeyboardSingleUse } = message || {}; const { isKeyboardSingleUse } = message || {};
const [forceOpen, markForceOpen, unmarkForceOpen] = useFlag(true);
const handleClose = useCallback(() => {
unmarkForceOpen();
onClose();
}, [onClose, unmarkForceOpen]);
useEffect(() => {
markForceOpen();
}, [markForceOpen, message?.keyboardButtons]);
if (!message || !message.keyboardButtons) { if (!message || !message.keyboardButtons) {
return undefined; return undefined;
@ -48,13 +37,13 @@ const BotKeyboardMenu: FC<OwnProps & StateProps> = ({
return ( return (
<Menu <Menu
isOpen={isOpen || forceOpen} isOpen={isOpen}
autoClose={isKeyboardSingleUse} autoClose={isKeyboardSingleUse}
positionX="right" positionX="right"
positionY="bottom" positionY="bottom"
onClose={handleClose} onClose={onClose}
className="BotKeyboardMenu" className="BotKeyboardMenu"
onCloseAnimationEnd={handleClose} onCloseAnimationEnd={onClose}
onMouseEnter={!IS_TOUCH_ENV ? handleMouseEnter : undefined} onMouseEnter={!IS_TOUCH_ENV ? handleMouseEnter : undefined}
onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined} onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined}
noCompact noCompact

View File

@ -56,6 +56,7 @@ const InvoiceMediaPreview: FC<OwnProps> = ({
openInvoice({ openInvoice({
chatId, chatId,
messageId: id, messageId: id,
isExtendedMedia: true,
}); });
}, [chatId, id, openInvoice]); }, [chatId, id, openInvoice]);

View File

@ -10,6 +10,8 @@ import './ConfirmPayment.scss';
export type OwnProps = { export type OwnProps = {
url: string; url: string;
noRedirect?: boolean;
onClose: NoneToVoidFunction;
}; };
interface IframeCallbackEvent { interface IframeCallbackEvent {
@ -19,8 +21,8 @@ interface IframeCallbackEvent {
}; };
} }
const ConfirmPayment: FC<OwnProps> = ({ url }) => { const ConfirmPayment: FC<OwnProps> = ({ url, noRedirect, onClose }) => {
const { closePaymentModal, openTelegramLink } = getActions(); const { openTelegramLink } = getActions();
const lang = useLang(); const lang = useLang();
@ -33,13 +35,16 @@ const ConfirmPayment: FC<OwnProps> = ({ url }) => {
return; return;
} }
const linkUrl = TME_LINK_PREFIX + eventData.path_full; if (!noRedirect) {
openTelegramLink({ url: linkUrl }); const linkUrl = TME_LINK_PREFIX + eventData.path_full;
closePaymentModal(); openTelegramLink({ url: linkUrl });
}
onClose();
} catch (err) { } catch (err) {
// Ignore other messages // Ignore other messages
} }
}, [closePaymentModal, openTelegramLink]); }, [onClose, noRedirect, openTelegramLink]);
useEffect(() => { useEffect(() => {
window.addEventListener('message', handleMessage); window.addEventListener('message', handleMessage);

View File

@ -4,7 +4,7 @@ $modalHeaderAndFooterHeight: 8.375rem;
&.recurring { &.recurring {
.Transition { .Transition {
height: 33rem; height: 33rem;
max-height: calc(90vh - $modalHeaderAndFooterHeight); max-height: calc(92vh - $modalHeaderAndFooterHeight);
} }
} }
@ -32,7 +32,7 @@ $modalHeaderAndFooterHeight: 8.375rem;
} }
.Transition { .Transition {
height: min(27rem, 60vh); height: min(27rem, 68vh);
} }
.empty-content { .empty-content {

View File

@ -39,7 +39,7 @@ const SUPPORTED_PROVIDERS = new Set([DEFAULT_PROVIDER, DONATE_PROVIDER]);
export type OwnProps = { export type OwnProps = {
isOpen?: boolean; isOpen?: boolean;
onClose: () => void; onClose: NoneToVoidFunction;
}; };
type StateProps = { type StateProps = {
@ -64,6 +64,7 @@ type StateProps = {
stripeId?: string; stripeId?: string;
savedCredentials?: ApiPaymentCredentials[]; savedCredentials?: ApiPaymentCredentials[];
passwordValidUntil?: number; passwordValidUntil?: number;
isExtendedMedia?: boolean;
}; };
type GlobalStateProps = Pick<GlobalState['payment'], ( type GlobalStateProps = Pick<GlobalState['payment'], (
@ -105,6 +106,7 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
stripeId, stripeId,
savedCredentials, savedCredentials,
passwordValidUntil, passwordValidUntil,
isExtendedMedia,
}) => { }) => {
const { const {
loadPasswordInfo, loadPasswordInfo,
@ -321,6 +323,8 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
return ( return (
<ConfirmPayment <ConfirmPayment
url={confirmPaymentUrl!} url={confirmPaymentUrl!}
noRedirect={isExtendedMedia}
onClose={closeModal}
/> />
); );
default: default:
@ -476,8 +480,27 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
? lang('Checkout.PayPrice', formatCurrency(totalPrice, currency!, lang.code)) ? lang('Checkout.PayPrice', formatCurrency(totalPrice, currency!, lang.code))
: lang('Next'); : lang('Next');
const isSubmitDisabled = isLoading function getIsSubmitDisabled() {
|| Boolean(step === PaymentStep.Checkout && invoice?.isRecurring && !isTosAccepted); if (isLoading) {
return true;
}
switch (step) {
case PaymentStep.Checkout:
return Boolean(invoice?.isRecurring && !isTosAccepted);
case PaymentStep.PaymentInfo:
return Boolean(
paymentState.cardNumber === ''
|| (needCardholderName && paymentState.cardholder === '')
|| paymentState.cvv === ''
|| paymentState.expiry === '',
);
default:
return false;
}
}
if (isProviderError) { if (isProviderError) {
return ( return (
@ -501,6 +524,8 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
); );
} }
const isSubmitDisabled = getIsSubmitDisabled();
return ( return (
<Modal <Modal
className={buildClassName('PaymentModal', invoice?.isRecurring && 'recurring')} className={buildClassName('PaymentModal', invoice?.isRecurring && 'recurring')}
@ -569,6 +594,7 @@ export default memo(withGlobal<OwnProps>(
smartGlocalCredentials, smartGlocalCredentials,
savedCredentials, savedCredentials,
temporaryPassword, temporaryPassword,
isExtendedMedia,
} = global.payment; } = global.payment;
const chat = inputInvoice && 'chatId' in inputInvoice ? selectChat(global, inputInvoice.chatId) : undefined; const chat = inputInvoice && 'chatId' in inputInvoice ? selectChat(global, inputInvoice.chatId) : undefined;
@ -615,6 +641,7 @@ export default memo(withGlobal<OwnProps>(
stripeId: stripeCredentials?.id, stripeId: stripeCredentials?.id,
savedCredentials, savedCredentials,
passwordValidUntil: temporaryPassword?.validUntil, passwordValidUntil: temporaryPassword?.validUntil,
isExtendedMedia,
}; };
}, },
)(PaymentModal)); )(PaymentModal));

View File

@ -144,7 +144,7 @@
flex-grow: 1; flex-grow: 1;
padding: 1rem; padding: 1rem;
overflow-y: auto; overflow-y: auto;
max-height: 90vh; max-height: 92vh;
b, b,
strong { strong {

View File

@ -25,7 +25,6 @@ import {
setPaymentForm, setPaymentForm,
setStripeCardInfo, setStripeCardInfo,
setReceipt, setReceipt,
clearPayment,
closeInvoice, closeInvoice,
setSmartGlocalCardInfo, addUsers, setInvoiceInfo, updatePayment, setSmartGlocalCardInfo, addUsers, setInvoiceInfo, updatePayment,
} from '../../reducers'; } from '../../reducers';
@ -82,6 +81,7 @@ addActionHandler('openInvoice', async (global, actions, payload) => {
inputInvoice: payload, inputInvoice: payload,
isPaymentModalOpen: true, isPaymentModalOpen: true,
status: 'cancelled', status: 'cancelled',
isExtendedMedia: (payload as any).isExtendedMedia,
}, },
}); });
}); });
@ -217,7 +217,6 @@ addActionHandler('sendPaymentForm', async (global, actions, payload) => {
} }
global = getGlobal(); global = getGlobal();
global = clearPayment(global);
global = updatePayment(global, { status: 'paid' }); global = updatePayment(global, { status: 'paid' });
global = closeInvoice(global); global = closeInvoice(global);
setGlobal(global); setGlobal(global);

View File

@ -1,7 +1,7 @@
import { addActionHandler } from '../../index'; import { addActionHandler } from '../../index';
import { IS_PRODUCTION_HOST } from '../../../util/environment'; import { IS_PRODUCTION_HOST } from '../../../util/environment';
import { clearPayment } from '../../reducers'; import { closeInvoice } from '../../reducers';
import * as langProvider from '../../../util/langProvider'; import * as langProvider from '../../../util/langProvider';
import { formatCurrency } from '../../../util/formatCurrency'; import { formatCurrency } from '../../../util/formatCurrency';
import { selectChatMessage } from '../../selectors'; import { selectChatMessage } from '../../selectors';
@ -28,8 +28,9 @@ addActionHandler('apiUpdate', (global, actions, update) => {
// On the production host, the payment frame receives a message with the payment event, // On the production host, the payment frame receives a message with the payment event,
// after which the payment form closes. In other cases, the payment form must be closed manually. // after which the payment form closes. In other cases, the payment form must be closed manually.
// Closing the invoice will cause the closing of the Payment Modal dialog and then closing the payment.
if (!IS_PRODUCTION_HOST) { if (!IS_PRODUCTION_HOST) {
global = clearPayment(global); global = closeInvoice(global);
} }
if (update.slug && inputInvoice && 'slug' in inputInvoice && inputInvoice.slug !== update.slug) { if (update.slug && inputInvoice && 'slug' in inputInvoice && inputInvoice.slug !== update.slug) {

View File

@ -110,5 +110,5 @@ export function clearPayment(global: GlobalState): GlobalState {
} }
export function closeInvoice(global: GlobalState): GlobalState { export function closeInvoice(global: GlobalState): GlobalState {
return updatePayment(global, { isPaymentModalOpen: undefined }); return updatePayment(global, { isPaymentModalOpen: undefined, isExtendedMedia: undefined });
} }

View File

@ -504,6 +504,7 @@ export type GlobalState = {
description?: string; description?: string;
}; };
isPaymentModalOpen?: boolean; isPaymentModalOpen?: boolean;
isExtendedMedia?: boolean;
confirmPaymentUrl?: string; confirmPaymentUrl?: string;
temporaryPassword?: { temporaryPassword?: {
value: string; value: string;