feat: add dedicated TopUpResult page for payment return flow

Replace toast-based payment return with animated result page showing
pending/success/failed/timeout states. Extract shared Spinner,
AnimatedCheckmark and AnimatedCrossmark components. Add sessionStorage
persistence with validation and TTL for cross-redirect payment data.
Unify ProtectedRoute with withLayout prop, add aria-live accessibility.
This commit is contained in:
Fringg
2026-03-09 03:58:40 +03:00
parent d7b1ff1052
commit b59122818c
14 changed files with 608 additions and 130 deletions

View File

@@ -43,6 +43,7 @@ const PurchaseSuccess = lazy(() => import('./pages/PurchaseSuccess'));
const AutoLogin = lazy(() => import('./pages/AutoLogin'));
const TopUpMethodSelect = lazy(() => import('./pages/TopUpMethodSelect'));
const TopUpAmount = lazy(() => import('./pages/TopUpAmount'));
const TopUpResult = lazy(() => import('./pages/TopUpResult'));
const ConnectedAccounts = lazy(() => import('./pages/ConnectedAccounts'));
const LinkTelegramCallback = lazy(() => import('./pages/LinkTelegramCallback'));
const MergeAccounts = lazy(() => import('./pages/MergeAccounts'));
@@ -112,7 +113,13 @@ const AdminLandings = lazy(() => import('./pages/AdminLandings'));
const AdminLandingEditor = lazy(() => import('./pages/AdminLandingEditor'));
const AdminLandingStats = lazy(() => import('./pages/AdminLandingStats'));
function ProtectedRoute({ children }: { children: React.ReactNode }) {
function ProtectedRoute({
children,
withLayout = true,
}: {
children: React.ReactNode;
withLayout?: boolean;
}) {
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
const isLoading = useAuthStore((state) => state.isLoading);
const location = useLocation();
@@ -122,12 +129,11 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) {
}
if (!isAuthenticated) {
// Сохраняем текущий URL для возврата после авторизации
saveReturnUrl();
return <Navigate to="/login" replace state={{ from: location.pathname }} />;
}
return <Layout>{children}</Layout>;
return withLayout ? <Layout>{children}</Layout> : <>{children}</>;
}
function AdminRoute({ children }: { children: React.ReactNode }) {
@@ -141,7 +147,6 @@ function AdminRoute({ children }: { children: React.ReactNode }) {
}
if (!isAuthenticated) {
// Сохраняем текущий URL для возврата после авторизации
saveReturnUrl();
return <Navigate to="/login" replace state={{ from: location.pathname }} />;
}
@@ -283,6 +288,18 @@ function App() {
</ProtectedRoute>
}
/>
<Route
path="/balance/top-up/result"
element={
<ProtectedRoute withLayout={false}>
<ErrorBoundary level="app">
<LazyPage>
<TopUpResult />
</LazyPage>
</ErrorBoundary>
</ProtectedRoute>
}
/>
<Route
path="/balance/top-up/:methodId"
element={