|
|
|
@ -12,6 +12,7 @@ import { UpgradeAccountModal } from './UpgradeAccountModal'; |
|
|
|
|
import { getUrlParams } from '../../lib/browser'; |
|
|
|
|
import { VerifyUpgrade } from './VerifyUpgrade'; |
|
|
|
|
import { EmptyBillingScreen } from './EmptyBillingScreen'; |
|
|
|
|
import { Calendar, RefreshCw, Loader2, AlertTriangle } from 'lucide-react'; |
|
|
|
|
|
|
|
|
|
export type CreateCustomerPortalBody = {}; |
|
|
|
|
|
|
|
|
@ -30,25 +31,28 @@ export function BillingPage() { |
|
|
|
|
queryClient, |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
const { mutate: createCustomerPortal, isPending: isCreatingCustomerPortal } = |
|
|
|
|
useMutation( |
|
|
|
|
{ |
|
|
|
|
mutationFn: (body: CreateCustomerPortalBody) => { |
|
|
|
|
return httpPost<CreateCustomerPortalResponse>( |
|
|
|
|
'/v1-create-customer-portal', |
|
|
|
|
body, |
|
|
|
|
); |
|
|
|
|
}, |
|
|
|
|
onSuccess: (data) => { |
|
|
|
|
window.location.href = data.url; |
|
|
|
|
}, |
|
|
|
|
onError: (error) => { |
|
|
|
|
console.error(error); |
|
|
|
|
toast.error(error?.message || 'Failed to Create Customer Portal'); |
|
|
|
|
}, |
|
|
|
|
const { |
|
|
|
|
mutate: createCustomerPortal, |
|
|
|
|
isSuccess: isCreatingCustomerPortalSuccess, |
|
|
|
|
isPending: isCreatingCustomerPortal, |
|
|
|
|
} = useMutation( |
|
|
|
|
{ |
|
|
|
|
mutationFn: (body: CreateCustomerPortalBody) => { |
|
|
|
|
return httpPost<CreateCustomerPortalResponse>( |
|
|
|
|
'/v1-create-customer-portal', |
|
|
|
|
body, |
|
|
|
|
); |
|
|
|
|
}, |
|
|
|
|
queryClient, |
|
|
|
|
); |
|
|
|
|
onSuccess: (data) => { |
|
|
|
|
window.location.href = data.url; |
|
|
|
|
}, |
|
|
|
|
onError: (error) => { |
|
|
|
|
console.error(error); |
|
|
|
|
toast.error(error?.message || 'Failed to Create Customer Portal'); |
|
|
|
|
}, |
|
|
|
|
}, |
|
|
|
|
queryClient, |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
if (isLoadingBillingDetails) { |
|
|
|
@ -97,75 +101,108 @@ export function BillingPage() { |
|
|
|
|
{showVerifyUpgradeModal && <VerifyUpgrade />} |
|
|
|
|
|
|
|
|
|
{billingDetails?.status === 'none' && !isLoadingBillingDetails && ( |
|
|
|
|
<EmptyBillingScreen
|
|
|
|
|
onUpgrade={() => setShowUpgradeModal(true)}
|
|
|
|
|
/> |
|
|
|
|
<EmptyBillingScreen onUpgrade={() => setShowUpgradeModal(true)} /> |
|
|
|
|
)} |
|
|
|
|
|
|
|
|
|
{billingDetails?.status !== 'none' && |
|
|
|
|
!isLoadingBillingDetails && |
|
|
|
|
priceDetails && ( |
|
|
|
|
<> |
|
|
|
|
<div className="mt-1 max-w-3xl"> |
|
|
|
|
{billingDetails?.status === 'past_due' && ( |
|
|
|
|
<div className="mb-4 rounded-md border border-red-300 bg-red-50 p-2 text-sm text-red-500"> |
|
|
|
|
We were not able to charge your card. Please update your payment |
|
|
|
|
information. |
|
|
|
|
<div className="mb-6 flex items-center gap-2 rounded-lg border border-red-300 bg-red-50 p-4 text-sm text-red-600"> |
|
|
|
|
<AlertTriangle className="h-5 w-5" /> |
|
|
|
|
<span> |
|
|
|
|
We were not able to charge your card.{' '} |
|
|
|
|
<button |
|
|
|
|
disabled={ |
|
|
|
|
isCreatingCustomerPortal || |
|
|
|
|
isCreatingCustomerPortalSuccess |
|
|
|
|
} |
|
|
|
|
onClick={() => { |
|
|
|
|
createCustomerPortal({}); |
|
|
|
|
}} |
|
|
|
|
className="font-semibold underline underline-offset-4 disabled:cursor-not-allowed disabled:opacity-50" |
|
|
|
|
> |
|
|
|
|
Update payment information. |
|
|
|
|
</button> |
|
|
|
|
</span> |
|
|
|
|
</div> |
|
|
|
|
)} |
|
|
|
|
|
|
|
|
|
<div className="flex items-start gap-10"> |
|
|
|
|
<div className="flex flex-col"> |
|
|
|
|
<span className="text-gray-500">Plan</span> |
|
|
|
|
<span className="mt-1 text-lg font-medium capitalize text-black"> |
|
|
|
|
{selectedPlanDetails?.name} |
|
|
|
|
</span> |
|
|
|
|
</div> |
|
|
|
|
<div className="flex grow items-center justify-between gap-2"> |
|
|
|
|
<div className="flex flex-col"> |
|
|
|
|
<span className="text-gray-500">Payment</span> |
|
|
|
|
<span className="mt-1 text-lg font-medium capitalize text-black"> |
|
|
|
|
<h2 className="mb-2 text-xl font-semibold text-black"> |
|
|
|
|
Current Subscription |
|
|
|
|
</h2> |
|
|
|
|
|
|
|
|
|
<p className="mb-6 text-sm text-gray-500"> |
|
|
|
|
Thank you for being a pro member. Your plan details are below. |
|
|
|
|
</p> |
|
|
|
|
|
|
|
|
|
<div className="flex flex-col gap-6 sm:flex-row sm:items-center sm:justify-between"> |
|
|
|
|
<div className="flex items-center gap-4"> |
|
|
|
|
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-gray-100"> |
|
|
|
|
<RefreshCw className="size-5 text-gray-600" /> |
|
|
|
|
</div> |
|
|
|
|
<div> |
|
|
|
|
<span className="text-xs uppercase tracking-wider text-gray-400"> |
|
|
|
|
Payment |
|
|
|
|
</span> |
|
|
|
|
<h3 className="flex items-baseline text-lg font-semibold text-black"> |
|
|
|
|
${priceDetails.amount} |
|
|
|
|
<span className="text-sm font-normal text-gray-500"> |
|
|
|
|
/ {priceDetails.interval} |
|
|
|
|
<span className="ml-1 text-sm font-normal text-gray-500"> |
|
|
|
|
/ {priceDetails.interval} |
|
|
|
|
</span> |
|
|
|
|
</h3> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<div className="mt-6 border-t border-gray-100 pt-6"> |
|
|
|
|
<div className="flex items-start gap-4"> |
|
|
|
|
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-gray-100"> |
|
|
|
|
<Calendar className="size-5 text-gray-600" /> |
|
|
|
|
</div> |
|
|
|
|
<div> |
|
|
|
|
<span className="text-xs uppercase tracking-wider text-gray-400"> |
|
|
|
|
{billingDetails?.cancelAtPeriodEnd |
|
|
|
|
? 'Expires On' |
|
|
|
|
: 'Renews On'} |
|
|
|
|
</span> |
|
|
|
|
<h3 className="text-lg font-semibold text-black"> |
|
|
|
|
{formattedNextBillDate} |
|
|
|
|
</h3> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<div className="mt-8 flex flex-wrap gap-3"> |
|
|
|
|
{!shouldHideDeleteButton && ( |
|
|
|
|
<button |
|
|
|
|
className="inline-flex items-center gap-1 self-end text-xs underline underline-offset-1 hover:text-gray-600" |
|
|
|
|
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-colors hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2" |
|
|
|
|
onClick={() => { |
|
|
|
|
setShowUpgradeModal(true); |
|
|
|
|
}} |
|
|
|
|
> |
|
|
|
|
Update Plan |
|
|
|
|
Switch Plan |
|
|
|
|
</button> |
|
|
|
|
)} |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<div className="mt-4 flex justify-between gap-2"> |
|
|
|
|
<div className="flex flex-col"> |
|
|
|
|
<span className="text-gray-500"> |
|
|
|
|
{billingDetails?.cancelAtPeriodEnd |
|
|
|
|
? 'Expires On' |
|
|
|
|
: 'Renews On'} |
|
|
|
|
</span> |
|
|
|
|
<span className="mt-1 text-lg font-medium capitalize text-black"> |
|
|
|
|
{formattedNextBillDate} |
|
|
|
|
</span> |
|
|
|
|
<button |
|
|
|
|
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-colors hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" |
|
|
|
|
onClick={() => { |
|
|
|
|
createCustomerPortal({}); |
|
|
|
|
}} |
|
|
|
|
disabled={ |
|
|
|
|
isCreatingCustomerPortal || isCreatingCustomerPortalSuccess |
|
|
|
|
} |
|
|
|
|
> |
|
|
|
|
{(isCreatingCustomerPortal || |
|
|
|
|
isCreatingCustomerPortalSuccess) && ( |
|
|
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> |
|
|
|
|
)} |
|
|
|
|
Manage Subscription |
|
|
|
|
</button> |
|
|
|
|
</div> |
|
|
|
|
<button |
|
|
|
|
className="inline-flex self-end text-xs underline underline-offset-1 hover:text-gray-600 disabled:cursor-not-allowed disabled:opacity-50" |
|
|
|
|
onClick={() => { |
|
|
|
|
createCustomerPortal({}); |
|
|
|
|
}} |
|
|
|
|
disabled={isCreatingCustomerPortal} |
|
|
|
|
> |
|
|
|
|
Manage my Subscription |
|
|
|
|
</button> |
|
|
|
|
</div> |
|
|
|
|
</> |
|
|
|
|
</div> |
|
|
|
|
)} |
|
|
|
|
</> |
|
|
|
|
); |
|
|
|
|