improve(payments): handling free products and subscriptions
This commit is contained in:
@@ -22,7 +22,7 @@ import Confirm from '@/modals/Confirm';
|
||||
import { api } from '@/trpc/client';
|
||||
import { cn } from '@/utils/cn';
|
||||
import type { IServiceOrganization } from '@openpanel/db';
|
||||
import type { IPolarPrice } from '@openpanel/payments';
|
||||
import { FREE_PRODUCT_IDS, type IPolarPrice } from '@openpanel/payments';
|
||||
import { format } from 'date-fns';
|
||||
import { Loader2Icon } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -152,41 +152,44 @@ export default function CurrentSubscription({ organization }: Props) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="col gap-2">
|
||||
{organization.isWillBeCanceled || organization.isCanceled ? (
|
||||
<Button
|
||||
loading={checkout.isLoading}
|
||||
onClick={() => {
|
||||
checkout.mutate({
|
||||
projectId,
|
||||
organizationId: organization.id,
|
||||
productPriceId: price!.id,
|
||||
productId: price.productId,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Reactivate subscription
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="destructive"
|
||||
loading={cancelSubscription.isLoading}
|
||||
onClick={() => {
|
||||
showConfirm({
|
||||
title: 'Cancel subscription',
|
||||
text: 'Are you sure you want to cancel your subscription?',
|
||||
onConfirm() {
|
||||
cancelSubscription.mutate({
|
||||
{organization.subscriptionProductId &&
|
||||
!FREE_PRODUCT_IDS.includes(organization.subscriptionProductId) && (
|
||||
<div className="col gap-2">
|
||||
{organization.isWillBeCanceled || organization.isCanceled ? (
|
||||
<Button
|
||||
loading={checkout.isLoading}
|
||||
onClick={() => {
|
||||
checkout.mutate({
|
||||
projectId,
|
||||
organizationId: organization.id,
|
||||
productPriceId: price!.id,
|
||||
productId: price.productId,
|
||||
});
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Cancel subscription
|
||||
</Button>
|
||||
}}
|
||||
>
|
||||
Reactivate subscription
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="destructive"
|
||||
loading={cancelSubscription.isLoading}
|
||||
onClick={() => {
|
||||
showConfirm({
|
||||
title: 'Cancel subscription',
|
||||
text: 'Are you sure you want to cancel your subscription?',
|
||||
onConfirm() {
|
||||
cancelSubscription.mutate({
|
||||
organizationId: organization.id,
|
||||
});
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Cancel subscription
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Dialog, DialogContent } from '@/components/ui/dialog';
|
||||
import { ModalHeader } from '@/modals/Modal/Container';
|
||||
import type { IServiceOrganization } from '@openpanel/db';
|
||||
import { useOpenPanel } from '@openpanel/nextjs';
|
||||
import { FREE_PRODUCT_IDS } from '@openpanel/payments';
|
||||
import Billing from './settings/organization/organization/billing';
|
||||
|
||||
interface SideEffectsProps {
|
||||
@@ -20,22 +21,36 @@ export default function SideEffects({ organization }: SideEffectsProps) {
|
||||
const willEndInHours = organization.subscriptionEndsAt
|
||||
? differenceInHours(organization.subscriptionEndsAt, new Date())
|
||||
: null;
|
||||
const [isTrialDialogOpen, setIsTrialDialogOpen] = useState<boolean>(
|
||||
willEndInHours !== null &&
|
||||
organization.subscriptionStatus === 'trialing' &&
|
||||
organization.subscriptionEndsAt !== null &&
|
||||
willEndInHours <= 48,
|
||||
);
|
||||
const [isTrialDialogOpen, setIsTrialDialogOpen] = useState<boolean>(false);
|
||||
const [isFreePlan, setIsFreePlan] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mounted) {
|
||||
setMounted(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (isTrialDialogOpen) {
|
||||
useEffect(() => {
|
||||
if (
|
||||
willEndInHours !== null &&
|
||||
organization.subscriptionStatus === 'trialing' &&
|
||||
organization.subscriptionEndsAt !== null &&
|
||||
willEndInHours <= 48
|
||||
) {
|
||||
setIsTrialDialogOpen(true);
|
||||
op.track('trial_expires_soon');
|
||||
}
|
||||
}, [isTrialDialogOpen]);
|
||||
}, [mounted, organization]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
organization.subscriptionProductId &&
|
||||
FREE_PRODUCT_IDS.includes(organization.subscriptionProductId)
|
||||
) {
|
||||
setIsFreePlan(true);
|
||||
op.track('free_plan_removed');
|
||||
}
|
||||
}, [mounted, organization]);
|
||||
|
||||
// Avoids hydration errors
|
||||
if (!mounted) {
|
||||
@@ -71,6 +86,29 @@ export default function SideEffects({ organization }: SideEffectsProps) {
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Dialog open={isFreePlan} onOpenChange={setIsFreePlan}>
|
||||
<DialogContent className="max-w-xl">
|
||||
<ModalHeader
|
||||
onClose={() => setIsFreePlan(false)}
|
||||
title={'Free plan has been removed'}
|
||||
text={
|
||||
<>
|
||||
Please upgrade your plan to continue using OpenPanel. Select a
|
||||
tier which is appropriate for your needs or{' '}
|
||||
<ProjectLink
|
||||
href="/settings/organization?tab=billing"
|
||||
className="underline text-foreground"
|
||||
>
|
||||
manage billing
|
||||
</ProjectLink>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<div className="-mx-4 mt-4">
|
||||
<Billing organization={organization} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -84,14 +84,25 @@ export async function createCheckout({
|
||||
});
|
||||
}
|
||||
|
||||
export function cancelSubscription(subscriptionId: string) {
|
||||
return polar.subscriptions.update({
|
||||
id: subscriptionId,
|
||||
subscriptionUpdate: {
|
||||
cancelAtPeriodEnd: true,
|
||||
revoke: null,
|
||||
},
|
||||
});
|
||||
export async function cancelSubscription(subscriptionId: string) {
|
||||
try {
|
||||
return await polar.subscriptions.update({
|
||||
id: subscriptionId,
|
||||
subscriptionUpdate: {
|
||||
cancelAtPeriodEnd: true,
|
||||
revoke: null,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
// Don't throw an error if the subscription is already canceled
|
||||
if (error.name === 'AlreadyCanceledSubscription') {
|
||||
return polar.subscriptions.get({ id: subscriptionId });
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function reactivateSubscription(subscriptionId: string) {
|
||||
|
||||
@@ -16,3 +16,8 @@ export const PRICING: IPrice[] = [
|
||||
// { price: 650, events: 20_000_000 },
|
||||
// { price: 900, events: 30_000_000 },
|
||||
];
|
||||
|
||||
export const FREE_PRODUCT_IDS = [
|
||||
'a18b4bee-d3db-4404-be6f-fba2f042d9ed', // Prod
|
||||
'036efa2a-b3b4-4c75-b24a-9cac6bb8893b', // Sandbox
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user