diff --git a/src/api/gramjs/apiBuilders/payments.ts b/src/api/gramjs/apiBuilders/payments.ts index f279a3975..483486657 100644 --- a/src/api/gramjs/apiBuilders/payments.ts +++ b/src/api/gramjs/apiBuilders/payments.ts @@ -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), diff --git a/src/api/types/payments.ts b/src/api/types/payments.ts index 46debc51d..b17f22d66 100644 --- a/src/api/types/payments.ts +++ b/src/api/types/payments.ts @@ -20,6 +20,7 @@ export interface ApiPaymentSavedInfo { } export interface ApiPaymentForm { + url: string; canSaveCredentials?: boolean; isPasswordMissing?: boolean; formId: string; diff --git a/src/components/payment/Checkout.tsx b/src/components/payment/Checkout.tsx index 2f850d515..1881f68b1 100644 --- a/src/components/payment/Checkout.tsx +++ b/src/components/payment/Checkout.tsx @@ -47,6 +47,7 @@ export type OwnProps = { dispatch?: FormEditDispatch; onAcceptTos?: (isAccepted: boolean) => void; savedCredentials?: ApiPaymentCredentials[]; + isPaymentFormUrl?: boolean; }; const Checkout: FC = ({ @@ -64,6 +65,7 @@ const Checkout: FC = ({ needAddress, hasShippingOptions, savedCredentials, + isPaymentFormUrl, }) => { const { setPaymentStep } = getActions(); @@ -185,7 +187,7 @@ const Checkout: FC = ({ )}
- {renderCheckoutItem({ + {!isPaymentFormUrl && renderCheckoutItem({ title: paymentMethod || savedCredentials?.[0].title, label: lang('PaymentCheckoutMethod'), icon: 'card', diff --git a/src/components/payment/ConfirmPayment.tsx b/src/components/payment/ConfirmPayment.tsx index 252eb1a4c..2fbad8951 100644 --- a/src/components/payment/ConfirmPayment.tsx +++ b/src/components/payment/ConfirmPayment.tsx @@ -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 = ({ url, noRedirect, onClose }) => { +interface WebAppOpenTgLinkEvent { + eventType: 'web_app_open_tg_link'; + eventData: { + path_full?: string; + }; +} + +type IframeCallbackEvent = PaymentFormSubmitEvent | WebAppOpenTgLinkEvent; + +const ConfirmPayment: FC = ({ + url, noRedirect, onClose, onPaymentFormSubmit, +}) => { const { openTelegramLink } = getActions(); const lang = useLang(); @@ -30,21 +46,27 @@ const ConfirmPayment: FC = ({ 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); diff --git a/src/components/payment/PaymentModal.tsx b/src/components/payment/PaymentModal.tsx index d9d2a7207..68edfbad1 100644 --- a/src/components/payment/PaymentModal.tsx +++ b/src/components/payment/PaymentModal.tsx @@ -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 = ({ savedCredentials, passwordValidUntil, isExtendedMedia, + isPaymentFormUrl, }) => { const { loadPasswordInfo, @@ -118,6 +122,7 @@ const PaymentModal: FC = ({ sendCredentialsInfo, clearPaymentError, validatePaymentPassword, + setSmartGlocalCardInfo, } = getActions(); const lang = useLang(); @@ -267,6 +272,21 @@ const PaymentModal: FC = ({ ); } + 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 = ({ totalPrice={totalPrice} invoice={invoice} checkoutInfo={checkoutInfo} + isPaymentFormUrl currency={currency!} hasShippingOptions={hasShippingOptions} tipAmount={paymentState.tipAmount} @@ -346,6 +367,7 @@ const PaymentModal: FC = ({ ); @@ -367,15 +389,6 @@ const PaymentModal: FC = ({ }); }, [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 = ({ 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 = ({ }, [ 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 = ({

{modalHeader}

{step !== undefined ? ( - +
{renderModalContent(step)}
@@ -622,10 +645,16 @@ export default memo(withGlobal( 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( shippingOptions, savedInfo, canSaveCredentials, - nativeProvider, + nativeProvider: providerName, passwordMissing, isNameRequested, isShippingAddressRequested, @@ -660,7 +689,8 @@ export default memo(withGlobal( needCountry, needZip, error, - confirmPaymentUrl, + confirmPaymentUrl: confirmPaymentUrl ?? url, + isPaymentFormUrl: Boolean(!nativeProvider && url), countryList: global.countryList.general, requestId, hasShippingOptions: Boolean(shippingOptions?.length), diff --git a/src/components/ui/Transition.tsx b/src/components/ui/Transition.tsx index 659bdc45e..0dc5df3af 100644 --- a/src/components/ui/Transition.tsx +++ b/src/components/ui/Transition.tsx @@ -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(() => { diff --git a/src/global/actions/api/payments.ts b/src/global/actions/api/payments.ts index 353684cc4..c34aa6cf4 100644 --- a/src/global/actions/api/payments.ts +++ b/src/global/actions/api/payments.ts @@ -185,10 +185,8 @@ addActionHandler('sendPaymentForm', async (global, actions, payload): Promise( 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); diff --git a/src/global/types.ts b/src/global/types.ts index 363944c9e..ce8f6ed3f 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -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;