From 0eba91a0ec433350058ce0f40beebb2cd07a73c6 Mon Sep 17 00:00:00 2001 From: john Date: Fri, 1 May 2026 05:06:15 +0700 Subject: [PATCH 1/2] refactor(subscribe): integrate Privy authentication for checkout sessions - Updated `useSubscribeClick` to retrieve an access token using `usePrivy` before creating a checkout session. - Modified `createClientCheckoutSession` to accept an access token and use it in the request headers for enhanced security. - Adjusted the success URL handling to ensure proper session creation and error handling. This change improves the authentication flow for subscription management. --- hooks/useSubscribeClick.ts | 12 +++- .../createClientCheckoutSession.test.ts | 70 +++++++++++++++++++ lib/stripe/createClientCheckoutSession.ts | 40 +++++++---- 3 files changed, 107 insertions(+), 15 deletions(-) create mode 100644 lib/stripe/__tests__/createClientCheckoutSession.test.ts diff --git a/hooks/useSubscribeClick.ts b/hooks/useSubscribeClick.ts index 3e160cb23..962f1c891 100644 --- a/hooks/useSubscribeClick.ts +++ b/hooks/useSubscribeClick.ts @@ -1,20 +1,26 @@ +import { usePrivy } from "@privy-io/react-auth"; import { useUserProvider } from "@/providers/UserProvder"; import { usePaymentProvider } from "@/providers/PaymentProvider"; import createClientPortalSession from "@/lib/stripe/createClientPortalSession"; import createClientCheckoutSession from "@/lib/stripe/createClientCheckoutSession"; const useSubscribeClick = () => { + const { getAccessToken } = usePrivy(); const { userData } = useUserProvider(); const { isSubscribed } = usePaymentProvider(); - const handleClick = () => { + const handleClick = async () => { if (!userData?.account_id) return; if (isSubscribed) { - createClientPortalSession(userData.account_id); + void createClientPortalSession(userData.account_id); return; } - createClientCheckoutSession(userData.account_id); + + const accessToken = await getAccessToken(); + if (!accessToken) return; + + void createClientCheckoutSession(accessToken, window.location.href); }; return { diff --git a/lib/stripe/__tests__/createClientCheckoutSession.test.ts b/lib/stripe/__tests__/createClientCheckoutSession.test.ts new file mode 100644 index 000000000..889185f0a --- /dev/null +++ b/lib/stripe/__tests__/createClientCheckoutSession.test.ts @@ -0,0 +1,70 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import createClientCheckoutSession from "../createClientCheckoutSession"; +import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl"; + +vi.mock("@/lib/api/getClientApiBaseUrl", () => ({ + getClientApiBaseUrl: vi.fn(), +})); + +describe("createClientCheckoutSession", () => { + const mockOpen = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + vi.spyOn(console, "error").mockImplementation(() => {}); + vi.mocked(getClientApiBaseUrl).mockReturnValue("https://api.recoupable.com"); + vi.stubGlobal("window", { open: mockOpen } as unknown as Window); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + }); + + it("POSTs only successUrl to recoup-api with Bearer auth and opens checkout url", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue({ + id: "cs_test_1", + url: "https://checkout.stripe.com/test", + }), + }) as unknown as typeof fetch; + + await createClientCheckoutSession( + "privy-token", + "https://chat.recoupable.com/billing", + ); + + expect(fetch).toHaveBeenCalledWith( + "https://api.recoupable.com/api/subscriptions/sessions", + expect.objectContaining({ + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer privy-token", + }, + }), + ); + const init = vi.mocked(fetch).mock.calls[0][1] as RequestInit; + expect(JSON.parse(String(init.body))).toEqual({ + successUrl: "https://chat.recoupable.com/billing", + }); + expect(mockOpen).toHaveBeenCalledWith( + "https://checkout.stripe.com/test", + "_blank", + ); + }); + + it("does not open a window and returns error when response is not ok", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 401, + json: vi.fn().mockResolvedValue({ error: "Unauthorized" }), + }) as unknown as typeof fetch; + + const result = await createClientCheckoutSession("bad-token", "https://ex.com"); + + expect(mockOpen).not.toHaveBeenCalled(); + expect(result).toEqual({ error: expect.any(Error) }); + }); +}); diff --git a/lib/stripe/createClientCheckoutSession.ts b/lib/stripe/createClientCheckoutSession.ts index 9f013774d..9d55e7d34 100644 --- a/lib/stripe/createClientCheckoutSession.ts +++ b/lib/stripe/createClientCheckoutSession.ts @@ -1,19 +1,35 @@ -const createClientCheckoutSession = async (accountId: string) => { +import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl"; + +const createClientCheckoutSession = async ( + accessToken: string, + successUrl: string, +) => { try { - const response = await fetch(`/api/stripe/session/create`, { - method: "POST", - headers: { - "Content-Type": "application/json", + const response = await fetch( + `${getClientApiBaseUrl()}/api/subscriptions/sessions`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ successUrl }), }, - body: JSON.stringify({ - accountId, - successUrl: `${window.location.href}`, - }), - }); + ); + + const data = (await response.json()) as { + id?: string; + url?: string; + error?: string; + }; + + if (!response.ok || typeof data.url !== "string") { + throw new Error(data.error || "Failed to create checkout session"); + } - const data = await response.json(); - window.open(data.data.url, "__blank"); + window.open(data.url, "_blank"); } catch (error) { + console.error("Error creating checkout session:", error); return { error }; } }; From a36332ca6efc2a1b55e1ad3de73ac7d01848a32b Mon Sep 17 00:00:00 2001 From: john Date: Fri, 1 May 2026 05:21:20 +0700 Subject: [PATCH 2/2] fix(stripe): enhance security for checkout session links - Updated the `window.open` method in `createClientCheckoutSession` to include `noopener,noreferrer` for improved security against potential tab-nabbing attacks. - Adjusted the corresponding test to verify the new parameters are correctly passed when opening the checkout session URL. This change enhances the security of the checkout process. --- lib/stripe/__tests__/createClientCheckoutSession.test.ts | 1 + lib/stripe/createClientCheckoutSession.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/stripe/__tests__/createClientCheckoutSession.test.ts b/lib/stripe/__tests__/createClientCheckoutSession.test.ts index 889185f0a..598184286 100644 --- a/lib/stripe/__tests__/createClientCheckoutSession.test.ts +++ b/lib/stripe/__tests__/createClientCheckoutSession.test.ts @@ -52,6 +52,7 @@ describe("createClientCheckoutSession", () => { expect(mockOpen).toHaveBeenCalledWith( "https://checkout.stripe.com/test", "_blank", + "noopener,noreferrer", ); }); diff --git a/lib/stripe/createClientCheckoutSession.ts b/lib/stripe/createClientCheckoutSession.ts index 9d55e7d34..eac5c0858 100644 --- a/lib/stripe/createClientCheckoutSession.ts +++ b/lib/stripe/createClientCheckoutSession.ts @@ -27,7 +27,7 @@ const createClientCheckoutSession = async ( throw new Error(data.error || "Failed to create checkout session"); } - window.open(data.url, "_blank"); + window.open(data.url, "_blank", "noopener,noreferrer"); } catch (error) { console.error("Error creating checkout session:", error); return { error };