From 62a6175d2aa9b020fbe2753ae603a44fface7a12 Mon Sep 17 00:00:00 2001 From: majdyz Date: Mon, 13 Apr 2026 21:47:15 +0700 Subject: [PATCH] fix(frontend): clear ?subscription=success URL param after showing toast Replace toastShownRef guard with router.replace(pathname) so the success toast is not re-shown on page refresh and correctly re-fires on a second checkout in the same SPA session. Adds test coverage for the behaviour. --- .../SubscriptionTierSection.test.tsx | 18 +++++++++- .../useSubscriptionTierSection.ts | 33 +++++++++---------- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/credits/components/SubscriptionTierSection/__tests__/SubscriptionTierSection.test.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/credits/components/SubscriptionTierSection/__tests__/SubscriptionTierSection.test.tsx index fbce376e84..5c038c0cca 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/credits/components/SubscriptionTierSection/__tests__/SubscriptionTierSection.test.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/credits/components/SubscriptionTierSection/__tests__/SubscriptionTierSection.test.tsx @@ -10,12 +10,13 @@ import { SubscriptionTierSection } from "../SubscriptionTierSection"; // Mock next/navigation const mockSearchParams = new URLSearchParams(); +const mockRouterReplace = vi.fn(); vi.mock("next/navigation", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useSearchParams: () => mockSearchParams, - useRouter: () => ({ push: vi.fn() }), + useRouter: () => ({ push: vi.fn(), replace: mockRouterReplace }), usePathname: () => "/profile/credits", }; }); @@ -103,6 +104,7 @@ afterEach(() => { mockUseGetSubscriptionStatus.mockReset(); mockUseUpdateSubscriptionTier.mockReset(); mockToast.mockReset(); + mockRouterReplace.mockReset(); // Reset search params mockSearchParams.delete("subscription"); }); @@ -289,4 +291,18 @@ describe("SubscriptionTierSection", () => { expect(screen.queryByText("Pro")).toBeNull(); expect(screen.queryByText("Business")).toBeNull(); }); + + it("shows success toast and clears URL param when ?subscription=success is present", async () => { + mockSearchParams.set("subscription", "success"); + setupMocks(); + render(); + + await waitFor(() => { + expect(mockToast).toHaveBeenCalledWith( + expect.objectContaining({ title: "Subscription upgraded" }), + ); + }); + // URL param must be stripped so a page refresh doesn't re-trigger the toast + expect(mockRouterReplace).toHaveBeenCalledWith("/profile/credits"); + }); }); diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/credits/components/SubscriptionTierSection/useSubscriptionTierSection.ts b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/credits/components/SubscriptionTierSection/useSubscriptionTierSection.ts index c22ab6f63f..a053848731 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/credits/components/SubscriptionTierSection/useSubscriptionTierSection.ts +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/credits/components/SubscriptionTierSection/useSubscriptionTierSection.ts @@ -1,5 +1,5 @@ -import { useEffect, useRef, useState } from "react"; -import { useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { useGetSubscriptionStatus, useUpdateSubscriptionTier, @@ -13,8 +13,9 @@ export type SubscriptionStatus = SubscriptionStatusResponse; export function useSubscriptionTierSection() { const searchParams = useSearchParams(); const subscriptionStatus = searchParams.get("subscription"); + const router = useRouter(); + const pathname = usePathname(); const { toast } = useToast(); - const toastShownRef = useRef(false); const [tierError, setTierError] = useState(null); const { @@ -36,22 +37,18 @@ export function useSubscriptionTierSection() { useEffect(() => { if (subscriptionStatus === "success") { - if (!toastShownRef.current) { - toastShownRef.current = true; - refetch(); - toast({ - title: "Subscription upgraded", - description: - "Your plan has been updated. It may take a moment to reflect.", - }); - } - } else { - // Reset so the toast fires again if the user completes another checkout - // during the same mount (e.g. SPA navigation away and back with a new - // ?subscription=success param). - toastShownRef.current = false; + refetch(); + toast({ + title: "Subscription upgraded", + description: + "Your plan has been updated. It may take a moment to reflect.", + }); + // Strip ?subscription=success from the URL so a page refresh does not + // re-trigger the toast, and so a second checkout in the same session + // correctly fires the toast again. + router.replace(pathname); } - }, [subscriptionStatus, refetch, toast]); + }, [subscriptionStatus, refetch, toast, router, pathname]); async function changeTier(tier: string) { setTierError(null);