Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,24 @@

import revalidate from '@/app/actions'
import {
useAuthenticatedCustomer,
useCustomerPaymentMethods,
} from '@/hooks/queries'
import { createClientSideAPI } from '@/utils/client'
import { schemas } from '@polar-sh/client'
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 { twMerge } from 'tailwind-merge'
import { Modal } from '../Modal'
import { useModal } from '../Modal/useModal'
import { Well, WellContent, WellHeader } from '../Shared/Well'
import { AddPaymentMethodModal } from './AddPaymentMethodModal'
import EditBillingDetails from './EditBillingDetails'
import PaymentMethod from './PaymentMethod'
useAuthenticatedCustomer,
useCustomerPaymentMethods,
useCustomerPortalSignOut,
} from "@/hooks/queries";
import { createClientSideAPI } from "@/utils/client";
import { schemas } from "@polar-sh/client";
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";
import { Well, WellContent, WellHeader } from "../Shared/Well";
import { AddPaymentMethodModal } from "./AddPaymentMethodModal";
import EditBillingDetails from "./EditBillingDetails";
import PaymentMethod from "./PaymentMethod";

interface CustomerPortalSettingsProps {
organization: schemas['Organization']
Expand All @@ -33,25 +35,41 @@ export const CustomerPortalSettings = ({
customerSessionToken,
setupIntentParams,
}: CustomerPortalSettingsProps) => {
const api = createClientSideAPI(customerSessionToken)
const api = createClientSideAPI(customerSessionToken);
const router = useRouter();

const {
isShown: isAddPaymentMethodModalOpen,
hide: hideAddPaymentMethodModal,
show: showAddPaymentMethodModal,
} = useModal(setupIntentParams !== undefined)
const { data: customer } = useAuthenticatedCustomer(api)
const { data: paymentMethods } = useCustomerPaymentMethods(api)
const {
isShown: isAddPaymentMethodModalOpen,
hide: hideAddPaymentMethodModal,
show: showAddPaymentMethodModal,
} = useModal();
const { data: customer } = useAuthenticatedCustomer(api);
const { data: paymentMethods } = useCustomerPaymentMethods(api);
const customerPortalSignOut = useCustomerPortalSignOut(api);

const theme = useTheme()
const themingPreset = useThemePreset(
organization.slug === 'midday' ? 'midday' : 'polar',
theme.resolvedTheme as 'light' | 'dark',
)

if (!customer) {
return null
}
const handleSignOut = async () => {
try {
await customerPortalSignOut.mutateAsync();

const url = new URL(window.location.href);
const parts = url.pathname.split("/");
const orgSlug = parts[1];

router.replace(`/${orgSlug}/portal/request`);
} catch (error) {
console.error("Failed to sign out:", error);
}
};

if (!customer) {
return null;
}

return (
<div className="flex flex-col gap-y-8">
Expand Down Expand Up @@ -110,22 +128,31 @@ export const CustomerPortalSettings = ({
</WellContent>
</Well>

<Modal
isShown={isAddPaymentMethodModalOpen}
hide={hideAddPaymentMethodModal}
modalContent={
<AddPaymentMethodModal
api={api}
onPaymentMethodAdded={() => {
revalidate(`customer_portal`)
hideAddPaymentMethodModal()
}}
setupIntentParams={setupIntentParams}
hide={hideAddPaymentMethodModal}
themingPreset={themingPreset}
/>
}
/>
</div>
)
}
<div className="flex">
<Button
variant="destructive"
onClick={handleSignOut}
disabled={customerPortalSignOut.isPending}
>
{customerPortalSignOut.isPending ? "Signing Out..." : "Sign Out"}
</Button>
</div>

<Modal
isShown={isAddPaymentMethodModalOpen}
hide={hideAddPaymentMethodModal}
modalContent={
<AddPaymentMethodModal
api={api}
onPaymentMethodAdded={() => {
revalidate(`customer_portal`);
hideAddPaymentMethodModal();
}}
hide={hideAddPaymentMethodModal}
themingPreset={themingPreset}
/>
}
/>
</div>
);
};
16 changes: 16 additions & 0 deletions clients/apps/web/src/hooks/queries/customerPortal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down Expand Up @@ -98,10 +112,12 @@ export const useDeleteCustomerPaymentMethod = (api: Client) =>
},
)
if (result.error) {

const errorMessage =
typeof result.error.detail === 'string'
? result.error.detail
: 'Failed to delete payment method'

throw new Error(errorMessage)
}
return result
Expand Down
17 changes: 17 additions & 0 deletions server/polar/customer_portal/endpoints/customer_session.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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)
Loading