PaymentModal: Fix payment (#4146)

This commit is contained in:
Alexander Zinchuk 2024-01-12 13:00:16 +01:00
parent 89e3640962
commit 35ae18dddc
8 changed files with 112 additions and 38 deletions

View File

@ -90,6 +90,7 @@ export function buildApiPaymentForm(form: GramJs.payments.PaymentForm): ApiPayme
savedInfo,
invoice,
savedCredentials,
url,
} = form;
const {
@ -118,6 +119,7 @@ export function buildApiPaymentForm(form: GramJs.payments.PaymentForm): ApiPayme
const nativeData = nativeParams ? JSON.parse(nativeParams.data) : {};
return {
url,
canSaveCredentials,
isPasswordMissing,
formId: String(formId),

View File

@ -20,6 +20,7 @@ export interface ApiPaymentSavedInfo {
}
export interface ApiPaymentForm {
url: string;
canSaveCredentials?: boolean;
isPasswordMissing?: boolean;
formId: string;

View File

@ -47,6 +47,7 @@ export type OwnProps = {
dispatch?: FormEditDispatch;
onAcceptTos?: (isAccepted: boolean) => void;
savedCredentials?: ApiPaymentCredentials[];
isPaymentFormUrl?: boolean;
};
const Checkout: FC<OwnProps> = ({
@ -64,6 +65,7 @@ const Checkout: FC<OwnProps> = ({
needAddress,
hasShippingOptions,
savedCredentials,
isPaymentFormUrl,
}) => {
const { setPaymentStep } = getActions();
@ -185,7 +187,7 @@ const Checkout: FC<OwnProps> = ({
)}
</div>
<div className={styles.invoiceInfo}>
{renderCheckoutItem({
{!isPaymentFormUrl && renderCheckoutItem({
title: paymentMethod || savedCredentials?.[0].title,
label: lang('PaymentCheckoutMethod'),
icon: 'card',

View File

@ -12,16 +12,32 @@ export type OwnProps = {
url: string;
noRedirect?: boolean;
onClose: NoneToVoidFunction;
onPaymentFormSubmit?: (eventData: PaymentFormSubmitEvent['eventData']) => void;
};
interface IframeCallbackEvent {
eventType: string;
export interface PaymentFormSubmitEvent {
eventType: 'payment_form_submit';
eventData: {
path_full: string;
credentials: {
token: string;
type: string;
};
title: string;
};
}
const ConfirmPayment: FC<OwnProps> = ({ url, noRedirect, onClose }) => {
interface WebAppOpenTgLinkEvent {
eventType: 'web_app_open_tg_link';
eventData: {
path_full?: string;
};
}
type IframeCallbackEvent = PaymentFormSubmitEvent | WebAppOpenTgLinkEvent;
const ConfirmPayment: FC<OwnProps> = ({
url, noRedirect, onClose, onPaymentFormSubmit,
}) => {
const { openTelegramLink } = getActions();
const lang = useLang();
@ -30,21 +46,27 @@ const ConfirmPayment: FC<OwnProps> = ({ url, noRedirect, onClose }) => {
try {
const data = JSON.parse(event.data) as IframeCallbackEvent;
const { eventType, eventData } = data;
if (eventType !== 'web_app_open_tg_link') {
return;
switch (eventType) {
case 'web_app_open_tg_link':
if (!noRedirect) {
const linkUrl = TME_LINK_PREFIX + eventData.path_full!;
openTelegramLink({ url: linkUrl });
}
onClose();
break;
case 'payment_form_submit':
if (onPaymentFormSubmit) {
onPaymentFormSubmit(eventData);
}
break;
default:
onClose();
break;
}
if (!noRedirect) {
const linkUrl = TME_LINK_PREFIX + eventData.path_full;
openTelegramLink({ url: linkUrl });
}
onClose();
} catch (err) {
// Ignore other messages
}
}, [onClose, noRedirect, openTelegramLink]);
}, [onClose, noRedirect, openTelegramLink, onPaymentFormSubmit]);
useEffect(() => {
window.addEventListener('message', handleMessage);

View File

@ -8,6 +8,7 @@ import type { ApiChat, ApiCountry, ApiPaymentCredentials } from '../../api/types
import type { TabState } from '../../global/types';
import type { FormState } from '../../hooks/reducers/usePaymentReducer';
import type { Price, ShippingOption } from '../../types';
import type { PaymentFormSubmitEvent } from './ConfirmPayment';
import { PaymentStep } from '../../types';
import { selectChat, selectTabState } from '../../global/selectors';
@ -37,6 +38,7 @@ import './PaymentModal.scss';
const DEFAULT_PROVIDER = 'stripe';
const DONATE_PROVIDER = 'smartglocal';
const DONATE_PROVIDER_URL = 'https://payment.smart-glocal.com';
const SUPPORTED_PROVIDERS = new Set([DEFAULT_PROVIDER, DONATE_PROVIDER]);
export type OwnProps = {
@ -67,6 +69,7 @@ type StateProps = {
savedCredentials?: ApiPaymentCredentials[];
passwordValidUntil?: number;
isExtendedMedia?: boolean;
isPaymentFormUrl?: boolean;
};
type GlobalStateProps = Pick<TabState['payment'], (
@ -109,6 +112,7 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
savedCredentials,
passwordValidUntil,
isExtendedMedia,
isPaymentFormUrl,
}) => {
const {
loadPasswordInfo,
@ -118,6 +122,7 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
sendCredentialsInfo,
clearPaymentError,
validatePaymentPassword,
setSmartGlocalCardInfo,
} = getActions();
const lang = useLang();
@ -267,6 +272,21 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
);
}
const sendForm = useCallback(() => {
sendPaymentForm({
shippingOptionId: paymentState.shipping,
saveCredentials: paymentState.saveCredentials,
savedCredentialId: paymentState.savedCredentialId,
tipAmount: paymentState.tipAmount,
});
}, [sendPaymentForm, paymentState]);
const handlePaymentFormSubmit = useCallback((eventData: PaymentFormSubmitEvent['eventData']) => {
const { credentials } = eventData;
setSmartGlocalCardInfo(credentials);
sendForm();
}, [sendForm]);
function renderModalContent(currentStep: PaymentStep) {
switch (currentStep) {
case PaymentStep.Checkout:
@ -281,6 +301,7 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
totalPrice={totalPrice}
invoice={invoice}
checkoutInfo={checkoutInfo}
isPaymentFormUrl
currency={currency!}
hasShippingOptions={hasShippingOptions}
tipAmount={paymentState.tipAmount}
@ -346,6 +367,7 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
<ConfirmPayment
url={confirmPaymentUrl!}
noRedirect={isExtendedMedia}
onPaymentFormSubmit={handlePaymentFormSubmit}
onClose={closeModal}
/>
);
@ -367,15 +389,6 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
});
}, [sendCredentialsInfo, paymentState]);
const sendForm = useCallback(() => {
sendPaymentForm({
shippingOptionId: paymentState.shipping,
saveCredentials: paymentState.saveCredentials,
savedCredentialId: paymentState.savedCredentialId,
tipAmount: paymentState.tipAmount,
});
}, [sendPaymentForm, paymentState]);
const handleButtonClick = useCallback(() => {
switch (step) {
case PaymentStep.ShippingInfo:
@ -407,6 +420,12 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
break;
case PaymentStep.Checkout: {
if (isPaymentFormUrl) {
setIsLoading(true);
setStep(PaymentStep.ConfirmPayment);
return;
}
if (savedInfo && !requestId && !paymentState.shipping) {
setIsLoading(true);
validateRequest();
@ -455,7 +474,7 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
}, [
isEmailRequested, isNameRequested, isPhoneRequested, isShippingAddressRequested, nativeProvider, passwordValidUntil,
paymentDispatch, paymentState, requestId, savedInfo, sendCredentials, sendForm, setStep, smartGlocalToken, step,
stripeId, twoFaPassword, validatePaymentPassword, validateRequest,
stripeId, twoFaPassword, validatePaymentPassword, validateRequest, isPaymentFormUrl,
]);
useEffect(() => {
@ -574,7 +593,11 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
<h3>{modalHeader}</h3>
</div>
{step !== undefined ? (
<Transition name="slide" activeKey={step}>
<Transition
name="slide"
activeKey={step}
cleanupKey={PaymentStep.ConfirmPayment}
>
<div className="content custom-scroll">
{renderModalContent(step)}
</div>
@ -622,10 +645,16 @@ export default memo(withGlobal<OwnProps>(
savedCredentials,
temporaryPassword,
isExtendedMedia,
url,
} = selectTabState(global).payment;
let providerName = nativeProvider;
if (!providerName && url) {
providerName = url.startsWith(DONATE_PROVIDER_URL) ? DONATE_PROVIDER : undefined;
}
const chat = inputInvoice && 'chatId' in inputInvoice ? selectChat(global, inputInvoice.chatId) : undefined;
const isProviderError = Boolean(invoice && (!nativeProvider || !SUPPORTED_PROVIDERS.has(nativeProvider)));
const isProviderError = Boolean(invoice && (!providerName || !SUPPORTED_PROVIDERS.has(providerName)));
const { needCardholderName, needCountry, needZip } = (nativeParams || {});
const {
isNameRequested,
@ -644,7 +673,7 @@ export default memo(withGlobal<OwnProps>(
shippingOptions,
savedInfo,
canSaveCredentials,
nativeProvider,
nativeProvider: providerName,
passwordMissing,
isNameRequested,
isShippingAddressRequested,
@ -660,7 +689,8 @@ export default memo(withGlobal<OwnProps>(
needCountry,
needZip,
error,
confirmPaymentUrl,
confirmPaymentUrl: confirmPaymentUrl ?? url,
isPaymentFormUrl: Boolean(!nativeProvider && url),
countryList: global.countryList.general,
requestId,
hasShippingOptions: Boolean(shippingOptions?.length),

View File

@ -34,6 +34,7 @@ export type TransitionProps = {
shouldRestoreHeight?: boolean;
shouldCleanup?: boolean;
cleanupExceptionKey?: number;
cleanupKey?: number;
// Used by async components which are usually remounted during first animation
shouldWrap?: boolean;
wrapExceptionKey?: number;
@ -73,6 +74,7 @@ function Transition({
shouldRestoreHeight,
shouldCleanup,
cleanupExceptionKey,
cleanupKey,
shouldWrap,
wrapExceptionKey,
id,
@ -120,13 +122,16 @@ function Transition({
useLayoutEffect(() => {
function cleanup() {
if (!shouldCleanup) {
if (cleanupKey !== undefined) {
delete rendersRef.current[cleanupKey];
}
return;
}
const preservedRender = cleanupExceptionKey !== undefined ? rendersRef.current[cleanupExceptionKey] : undefined;
rendersRef.current = preservedRender ? { [cleanupExceptionKey!]: preservedRender } : {};
if (cleanupExceptionKey !== undefined) {
rendersRef.current = { [cleanupExceptionKey]: rendersRef.current[cleanupExceptionKey] };
} else {
rendersRef.current = {};
}
forceUpdate();
}
@ -312,6 +317,7 @@ function Transition({
shouldDisableAnimation,
forceUpdate,
withSwipeControl,
cleanupKey,
]);
useEffect(() => {

View File

@ -185,10 +185,8 @@ addActionHandler('sendPaymentForm', async (global, actions, payload): Promise<vo
const formId = selectPaymentFormId(global, tabId);
const requestInfoId = selectPaymentRequestId(global, tabId);
const { nativeProvider, temporaryPassword } = selectTabState(global, tabId).payment;
const publishableKey = nativeProvider === 'stripe'
? selectProviderPublishableKey(global, tabId) : selectProviderPublicToken(global, tabId);
if (!inputInvoice || !publishableKey || !formId || !nativeProvider) {
if (!inputInvoice || !formId) {
return;
}
@ -341,6 +339,14 @@ async function sendSmartGlocalCredentials<T extends GlobalState>(
setGlobal(global);
}
addActionHandler('setSmartGlocalCardInfo', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId(), type, token } = payload;
return setSmartGlocalCardInfo(global, {
type,
token,
}, tabId);
});
addActionHandler('setPaymentStep', (global, actions, payload): ActionReturnType => {
const { step, tabId = getCurrentTabId() } = payload;
return setPaymentStep(global, step ?? PaymentStep.Checkout, tabId);

View File

@ -472,6 +472,7 @@ export type TabState = {
value: string;
validUntil: number;
};
url?: string;
};
chatCreation?: {
@ -1504,6 +1505,10 @@ export interface ActionPayloads {
sendCredentialsInfo: {
credentials: ApiCredentials;
} & WithTabId;
setSmartGlocalCardInfo: {
type: string;
token: string;
} & WithTabId;
clearPaymentError: WithTabId | undefined;
clearReceipt: WithTabId | undefined;