From faac3321389dc23976c81999e222305e72446812 Mon Sep 17 00:00:00 2001 From: Kirill Date: Wed, 20 May 2026 19:28:46 +0500 Subject: [PATCH] feat: implement yookassa redirect payment flow on client --- client/src/entities/order/api/order-api.ts | 12 +++++ .../order-payment/ui/OrderPaymentSection.tsx | 33 +++++------- .../pages/me/ui/sections/OrderDetailPage.tsx | 50 +++++++++++++++---- 3 files changed, 64 insertions(+), 31 deletions(-) diff --git a/client/src/entities/order/api/order-api.ts b/client/src/entities/order/api/order-api.ts index 3b5f2fb..a57bc29 100644 --- a/client/src/entities/order/api/order-api.ts +++ b/client/src/entities/order/api/order-api.ts @@ -69,6 +69,18 @@ export async function fetchMyOrder(id: string): Promise { return data } +/** Создать платёж в ЮKassa и получить URL для редиректа на форму оплаты. */ +export async function createOrderPayment(orderId: string): Promise<{ confirmationUrl: string }> { + const { data } = await apiClient.post<{ confirmationUrl: string }>(`me/orders/${orderId}/pay`) + return data +} + +/** Получить статус платежа для заказа. */ +export async function getOrderPaymentStatus(orderId: string): Promise<{ status: string | null; paid: boolean }> { + const { data } = await apiClient.get<{ status: string | null; paid: boolean }>(`me/orders/${orderId}/payment`) + return data +} + export async function postOrderMessage(id: string, text: string): Promise { await apiClient.post(`me/orders/${id}/messages`, { text }) } diff --git a/client/src/features/order-payment/ui/OrderPaymentSection.tsx b/client/src/features/order-payment/ui/OrderPaymentSection.tsx index 35873a2..4f13e7b 100644 --- a/client/src/features/order-payment/ui/OrderPaymentSection.tsx +++ b/client/src/features/order-payment/ui/OrderPaymentSection.tsx @@ -1,9 +1,7 @@ -import { useState } from 'react' import Box from '@mui/material/Box' import Button from '@mui/material/Button' import Typography from '@mui/material/Typography' import { orderStatusLabelRu } from '@/shared/lib/order-status-labels' -import { PaymentDialog } from './PaymentDialog' type Props = { status: string @@ -12,7 +10,7 @@ type Props = { totalCents: number isPayPending: boolean payError: unknown - onPay: (params: { detail: string; receiptFile: File | null }) => void + onPay: () => void } export function OrderPaymentSection({ @@ -24,7 +22,6 @@ export function OrderPaymentSection({ onPay, }: Props) { const payOnPickup = (paymentMethod ?? 'online') === 'on_pickup' - const [payModalOpen, setPayModalOpen] = useState(false) if (payOnPickup) { return ( @@ -52,30 +49,24 @@ export function OrderPaymentSection({ {status === 'PENDING_PAYMENT' && deliveryFeeLocked === true && ( <> - После перевода подтвердите оплату — откроется форма для комментария и фото чека. Заказ получит статус « + Вы будете перенаправлены на защищённую платёжную страницу ЮKassa. После оплаты заказ получит статус « {orderStatusLabelRu('PAID')}». - )} - {status !== 'PENDING_PAYMENT' && ( - - На этом этапе действий по оплате в этом блоке не требуется. + {status === 'PAID' && ( + + Оплачено. Спасибо! + + )} + {status !== 'PENDING_PAYMENT' && status !== 'PAID' && ( + + На этом этапе действий по оплате не требуется. )} - - setPayModalOpen(false)} - onSubmit={(params) => { - onPay(params) - setPayModalOpen(false) - }} - /> ) } diff --git a/client/src/pages/me/ui/sections/OrderDetailPage.tsx b/client/src/pages/me/ui/sections/OrderDetailPage.tsx index d409012..353c515 100644 --- a/client/src/pages/me/ui/sections/OrderDetailPage.tsx +++ b/client/src/pages/me/ui/sections/OrderDetailPage.tsx @@ -7,12 +7,13 @@ import Link from '@mui/material/Link' import Stack from '@mui/material/Stack' import Typography from '@mui/material/Typography' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { Link as RouterLink, useParams } from 'react-router-dom' +import { Link as RouterLink, useParams, useSearchParams } from 'react-router-dom' import { confirmOrderReceived, + createOrderPayment, fetchMyOrder, + getOrderPaymentStatus, postOrderMessage, - submitOrderPayment, fetchOrderReviewEligibility, } from '@/entities/order/api/order-api' import { postProductReview, uploadReviewImage } from '@/entities/review/api/reviews-api' @@ -30,6 +31,9 @@ export function OrderDetailPage() { const { id } = useParams() const qc = useQueryClient() + const [searchParams] = useSearchParams() + const paidParam = searchParams.get('paid') + const orderQuery = useQuery({ queryKey: ['me', 'orders', id], queryFn: () => fetchMyOrder(id!), @@ -37,13 +41,20 @@ export function OrderDetailPage() { }) const payMut = useMutation({ - mutationFn: (params: { detail: string; receiptFile: File | null }) => submitOrderPayment(id!, params), - onSuccess: async () => { - await Promise.all([ - qc.invalidateQueries({ queryKey: ['me', 'orders', id] }), - qc.invalidateQueries({ queryKey: ['me', 'orders'] }), - qc.invalidateQueries({ queryKey: ['me', 'conversations'] }), - ]) + mutationFn: () => createOrderPayment(id!), + onSuccess: async (data) => { + window.location.href = data.confirmationUrl + }, + }) + + const paymentStatusQuery = useQuery({ + queryKey: ['me', 'orders', id, 'payment-status'], + queryFn: () => getOrderPaymentStatus(id!), + enabled: Boolean(id && paidParam === '1'), + refetchInterval: (query) => { + const data = query.state.data + if (data && (data.paid || data.status === 'canceled')) return false + return 3000 }, }) @@ -117,6 +128,25 @@ export function OrderDetailPage() { + {paidParam === '1' && paymentStatusQuery.data && ( + + {paymentStatusQuery.data.paid + ? 'Оплата прошла успешно!' + : paymentStatusQuery.data.status === 'canceled' + ? 'Оплата отмена. Вы можете попробовать снова.' + : 'Ожидаем подтверждения оплаты…'} + + )} + @@ -212,7 +242,7 @@ export function OrderDetailPage() { totalCents={order.totalCents} isPayPending={payMut.isPending} payError={payMut.error} - onPay={(params) => payMut.mutate(params)} + onPay={() => payMut.mutate()} /> {(order.deliveryType === 'delivery' && order.status === 'SHIPPED') ||