feat(driver-pwa): payment screen + auto status polling
Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { getPaymentState } from "@/api/driver";
|
||||||
|
|
||||||
|
export function usePaymentStatus(orderId: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["payment", orderId],
|
||||||
|
queryFn: () => getPaymentState(orderId),
|
||||||
|
refetchInterval: (q) => (q.state.data?.status === "PAID" || q.state.data?.status === "REJECTED" ? false : 1500),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { describe, it, expect, vi } from "vitest";
|
||||||
|
import { render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { MemoryRouter, Routes, Route } from "react-router-dom";
|
||||||
|
|
||||||
|
const states = ["FORM", "PAID"];
|
||||||
|
vi.mock("@/api/driver", () => ({
|
||||||
|
getPaymentState: vi.fn(async () => ({ order_id: "o9", status: states.shift() ?? "PAID" })),
|
||||||
|
}));
|
||||||
|
import { PaymentPage } from "./PaymentPage";
|
||||||
|
|
||||||
|
function wrap() {
|
||||||
|
const qc = new QueryClient();
|
||||||
|
return render(
|
||||||
|
<QueryClientProvider client={qc}>
|
||||||
|
<MemoryRouter initialEntries={[{ pathname: "/pay/o9", state: { payUrl: "https://qr.nspk.ru/x", total: 1052, bucket: "Долг аренда", base: 1000 } }]}>
|
||||||
|
<Routes><Route path="/pay/:orderId" element={<PaymentPage />} /></Routes>
|
||||||
|
</MemoryRouter>
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("PaymentPage", () => {
|
||||||
|
it("shows pay button and flips to success on PAID", async () => {
|
||||||
|
wrap();
|
||||||
|
expect(screen.getByRole("link", { name: /Оплатить/i })).toHaveAttribute("href", "https://qr.nspk.ru/x");
|
||||||
|
await waitFor(() => expect(screen.getByText(/Оплачено/i)).toBeInTheDocument(), { timeout: 3000 });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1 +1,42 @@
|
|||||||
export function PaymentPage() { return null; }
|
import { useEffect } from "react";
|
||||||
|
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { Check } from "lucide-react";
|
||||||
|
import { formatMoney } from "@/lib/format";
|
||||||
|
import { Spinner } from "@/components/Spinner";
|
||||||
|
import { usePaymentStatus } from "@/hooks/usePaymentStatus";
|
||||||
|
|
||||||
|
export function PaymentPage() {
|
||||||
|
const { orderId = "" } = useParams();
|
||||||
|
const nav = useNavigate();
|
||||||
|
const qc = useQueryClient();
|
||||||
|
const st = (useLocation().state as { payUrl?: string; total?: number; bucket?: string; base?: number } | null) ?? {};
|
||||||
|
const { data } = usePaymentStatus(orderId);
|
||||||
|
const paid = data?.status === "PAID";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (paid) qc.invalidateQueries({ queryKey: ["balance"] });
|
||||||
|
}, [paid, qc]);
|
||||||
|
|
||||||
|
if (paid) {
|
||||||
|
return (
|
||||||
|
<div className="pt-24 flex flex-col items-center text-center">
|
||||||
|
<span className="w-14 h-14 rounded-full bg-pos text-white flex items-center justify-center mb-3"><Check size={28} /></span>
|
||||||
|
<p className="text-lg font-extrabold">Оплачено</p>
|
||||||
|
<p className="text-muted text-xs mt-1">{st.bucket} пополнен на {formatMoney(st.base ?? 0)}</p>
|
||||||
|
<button className="btn-primary mt-8" onClick={() => nav("/")}>На главную</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pt-10 flex flex-col items-center text-center">
|
||||||
|
<span className="w-12 h-12 rounded-xl2 bg-ink text-cream flex items-center justify-center font-extrabold mb-4">СБП</span>
|
||||||
|
<p className="text-lg font-extrabold">{formatMoney(st.total ?? 0)}</p>
|
||||||
|
<p className="text-muted text-xs mb-6">{st.bucket}</p>
|
||||||
|
<a className="btn-primary no-underline" href={st.payUrl} target="_blank" rel="noopener">Оплатить</a>
|
||||||
|
<p className="text-muted text-xs mt-2">Откроется ваше приложение банка</p>
|
||||||
|
<div className="flex items-center gap-2 mt-6 text-muted text-xs"><Spinner /> Ожидаем оплату…</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user