From 96dd90c7744654869d3f4541ebd07d70f1e4e3fb Mon Sep 17 00:00:00 2001 From: Manas Kenge Date: Sat, 13 Sep 2025 19:18:27 +0530 Subject: [PATCH 1/2] add sign out --- .../CustomerPortal/CustomerPortalSettings.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/clients/apps/web/src/components/CustomerPortal/CustomerPortalSettings.tsx b/clients/apps/web/src/components/CustomerPortal/CustomerPortalSettings.tsx index 437b18be57..37021272a5 100644 --- a/clients/apps/web/src/components/CustomerPortal/CustomerPortalSettings.tsx +++ b/clients/apps/web/src/components/CustomerPortal/CustomerPortalSettings.tsx @@ -11,6 +11,7 @@ import Button from "@polar-sh/ui/components/atoms/Button"; import { Separator } from "@polar-sh/ui/components/ui/separator"; import { useThemePreset } from "@polar-sh/ui/hooks/theming"; import { useTheme } from "next-themes"; +import { useRouter } from "next/navigation"; import { twMerge } from "tailwind-merge"; import { Modal } from "../Modal"; import { useModal } from "../Modal/useModal"; @@ -29,6 +30,7 @@ export const CustomerPortalSettings = ({ customerSessionToken, }: CustomerPortalSettingsProps) => { const api = createClientSideAPI(customerSessionToken); + const router = useRouter(); const { isShown: isAddPaymentMethodModalOpen, @@ -44,6 +46,15 @@ export const CustomerPortalSettings = ({ theme.resolvedTheme as "light" | "dark", ); + const handleSignOut = () => { + const url = new URL(window.location.href); + + const parts = url.pathname.split("/"); + const orgSlug = parts[1]; + + router.replace(`/${orgSlug}/portal/request`); + }; + if (!customer) { return null; } @@ -105,6 +116,15 @@ export const CustomerPortalSettings = ({ +
+ +
+ Date: Sat, 20 Sep 2025 15:14:19 +0530 Subject: [PATCH 2/2] add signout endpoint --- .../CustomerPortal/CustomerPortalSettings.tsx | 20 +++++++++++++------ .../web/src/hooks/queries/customerPortal.ts | 18 +++++++++++++++-- .../endpoints/customer_session.py | 17 ++++++++++++++++ 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/clients/apps/web/src/components/CustomerPortal/CustomerPortalSettings.tsx b/clients/apps/web/src/components/CustomerPortal/CustomerPortalSettings.tsx index 37021272a5..a7e7dc7e48 100644 --- a/clients/apps/web/src/components/CustomerPortal/CustomerPortalSettings.tsx +++ b/clients/apps/web/src/components/CustomerPortal/CustomerPortalSettings.tsx @@ -4,6 +4,7 @@ import revalidate from "@/app/actions"; import { useAuthenticatedCustomer, useCustomerPaymentMethods, + useCustomerPortalSignOut, } from "@/hooks/queries"; import { createClientSideAPI } from "@/utils/client"; import { schemas } from "@polar-sh/client"; @@ -39,6 +40,7 @@ export const CustomerPortalSettings = ({ } = useModal(); const { data: customer } = useAuthenticatedCustomer(api); const { data: paymentMethods } = useCustomerPaymentMethods(api); + const customerPortalSignOut = useCustomerPortalSignOut(api); const theme = useTheme(); const themingPreset = useThemePreset( @@ -46,13 +48,18 @@ export const CustomerPortalSettings = ({ theme.resolvedTheme as "light" | "dark", ); - const handleSignOut = () => { - const url = new URL(window.location.href); + const handleSignOut = async () => { + try { + await customerPortalSignOut.mutateAsync(); - const parts = url.pathname.split("/"); - const orgSlug = parts[1]; + const url = new URL(window.location.href); + const parts = url.pathname.split("/"); + const orgSlug = parts[1]; - router.replace(`/${orgSlug}/portal/request`); + router.replace(`/${orgSlug}/portal/request`); + } catch (error) { + console.error("Failed to sign out:", error); + } }; if (!customer) { @@ -120,8 +127,9 @@ export const CustomerPortalSettings = ({ diff --git a/clients/apps/web/src/hooks/queries/customerPortal.ts b/clients/apps/web/src/hooks/queries/customerPortal.ts index ef0e6b051b..e1a41afd14 100644 --- a/clients/apps/web/src/hooks/queries/customerPortal.ts +++ b/clients/apps/web/src/hooks/queries/customerPortal.ts @@ -48,6 +48,20 @@ export const useUpdateCustomerPortal = (api: Client) => }, }) +export const useCustomerPortalSignOut = (api: Client) => + useMutation({ + mutationFn: async () => + api.DELETE('/v1/customer-portal/customer-session/sign-out'), + onSuccess: async (result, _variables, _ctx) => { + if (result.error) { + return + } + queryClient.invalidateQueries({ + queryKey: ['customer'], + }) + }, + }) + export const useCustomerPaymentMethods = (api: Client) => useQuery({ queryKey: ['customer_payment_methods'], @@ -79,8 +93,8 @@ export const useDeleteCustomerPaymentMethod = (api: Client) => params: { path: { id } }, }) if (result.error) { - const errorMessage = typeof result.error.detail === 'string' - ? result.error.detail + const errorMessage = typeof result.error.detail === 'string' + ? result.error.detail : 'Failed to delete payment method' throw new Error(errorMessage) } diff --git a/server/polar/customer_portal/endpoints/customer_session.py b/server/polar/customer_portal/endpoints/customer_session.py index 263409683a..638ebd3408 100644 --- a/server/polar/customer_portal/endpoints/customer_session.py +++ b/server/polar/customer_portal/endpoints/customer_session.py @@ -1,10 +1,13 @@ from fastapi import Depends +from sqlalchemy import delete from polar.kit.db.postgres import AsyncSession +from polar.models import CustomerSession from polar.openapi import APITag from polar.postgres import get_db_session from polar.routing import APIRouter +from .. import auth from ..schemas.customer_session import ( CustomerSessionCodeAuthenticateRequest, CustomerSessionCodeAuthenticateResponse, @@ -51,3 +54,17 @@ async def authenticate( session, authenticated_request.code ) return CustomerSessionCodeAuthenticateResponse(token=token) + + +@router.delete("/sign-out", name="customer_portal.customer_session.sign_out", status_code=204) +async def sign_out( + auth_subject: auth.CustomerPortalWrite, + session: AsyncSession = Depends(get_db_session), +) -> None: + customer = auth_subject.subject + + statement = delete(CustomerSession).where( + CustomerSession.customer_id == customer.id, + CustomerSession.deleted_at.is_(None) + ) + await session.execute(statement)