diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/organization/current-subscription.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/organization/current-subscription.tsx
index a40c73c2..2f85beda 100644
--- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/organization/current-subscription.tsx
+++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/organization/current-subscription.tsx
@@ -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) {
)}
-
- {organization.isWillBeCanceled || organization.isCanceled ? (
-
- ) : (
-
>
);
}
diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects.tsx
index 497080af..0efbc9c3 100644
--- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects.tsx
+++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects.tsx
@@ -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(
- willEndInHours !== null &&
- organization.subscriptionStatus === 'trialing' &&
- organization.subscriptionEndsAt !== null &&
- willEndInHours <= 48,
- );
+ const [isTrialDialogOpen, setIsTrialDialogOpen] = useState(false);
+ const [isFreePlan, setIsFreePlan] = useState(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) {
+
>
);
}
diff --git a/packages/payments/src/polar.ts b/packages/payments/src/polar.ts
index 15b67709..7265785a 100644
--- a/packages/payments/src/polar.ts
+++ b/packages/payments/src/polar.ts
@@ -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) {
diff --git a/packages/payments/src/prices.ts b/packages/payments/src/prices.ts
index c42f3e46..242840b7 100644
--- a/packages/payments/src/prices.ts
+++ b/packages/payments/src/prices.ts
@@ -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
+];