Skip to content

Commit

Permalink
show free trial dialog only in stripe test mode
Browse files Browse the repository at this point in the history
  • Loading branch information
imbhargav5 committed Aug 12, 2024
1 parent fb759b4 commit cff17fb
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
import { SidebarLink } from '@/components/SidebarLink';
import { fetchSlimOrganizations, getNormalizedOrganizationSubscription, getOrganizationSlugByOrganizationId } from '@/data/user/organizations';
import { fetchSlimOrganizations, getActiveProductsWithPrices, getLoggedInUserOrganizationRole, getNormalizedOrganizationSubscription, getOrganizationSlugByOrganizationId } from '@/data/user/organizations';
import { cn } from '@/utils/cn';
import { organizationParamSchema } from '@/utils/zod-schemas/params';
import { notFound } from 'next/navigation';
import { Suspense } from 'react';

import { FreeTrialDialog } from '@/components/FreeTrialDialog';
import { DesktopSidebarFallback } from '@/components/SidebarComponents/SidebarFallback';
import { SwitcherAndToggle } from '@/components/SidebarComponents/SidebarLogo';
import { FreeTrialComponent } from '@/components/SubscriptionCards';
import { Skeleton } from '@/components/ui/skeleton';
import { getIsStripeTestMode } from '@/utils/server/stripe-utils';
import { differenceInDays } from 'date-fns';
import { Activity, FileText, GitCompare, Home, Layers, MessageCircle, Settings, Shield, Users } from 'lucide-react';

async function OrganizationSubscription({

const isStripeTestMode = getIsStripeTestMode()

async function OrganizationSubscriptionSidebarCard({
organizationId,
}: {
organizationId: string;
}) {
const normalizedSubscription =
await getNormalizedOrganizationSubscription(organizationId);
const [normalizedSubscription, activeProducts, userRole] = await Promise.all([
getNormalizedOrganizationSubscription(organizationId),
getActiveProductsWithPrices(),
getLoggedInUserOrganizationRole(organizationId)
]);

const isOrganizationAdmin = userRole === 'admin' || userRole === 'owner'

switch (normalizedSubscription.type) {
case 'trialing':
Expand All @@ -28,8 +37,17 @@ async function OrganizationSubscription({
planName={normalizedSubscription.product.name ?? 'Digger Plan'}
daysRemaining={differenceInDays(new Date(normalizedSubscription.trialEnd), new Date())}
/>
default:
case 'active':
return null;
default:
return <>
<FreeTrialDialog
isOrganizationAdmin={isOrganizationAdmin}
organizationId={organizationId}
activeProducts={activeProducts}
defaultOpen={isStripeTestMode}
/>
</>
}

}
Expand Down Expand Up @@ -107,7 +125,7 @@ async function OrganizationSidebarInternal({
</div>
<div>
<Suspense fallback={<Skeleton className="h-10 w-full" />}>
<OrganizationSubscription organizationId={organizationId} />
<OrganizationSubscriptionSidebarCard organizationId={organizationId} />
</Suspense>
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
'use server';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { T } from '@/components/ui/Typography';
import { DIGGER_FEATURES } from '@/constants';
import { getActiveProductsWithPrices } from '@/data/user/organizations';
import type { Enum, NormalizedSubscription, UnwrapPromise } from '@/types';
import { cn } from '@/utils/cn';
import { formatNormalizedSubscription } from '@/utils/formatNormalizedSubscription';
import { Check, X } from 'lucide-react';
import {
ManageSubscriptionButton,
StartFreeTrialButton
Expand Down Expand Up @@ -104,34 +104,18 @@ async function ChoosePricingTable({
</div>

<div className="px-5 pl-6 pt-0 mb-8">
<ul className="font-medium text-muted-foreground">
<li className="grid grid-cols-[24px,1fr] gap-0 text-md items-start mb-2">
<Check className="text-green-600 w-6 h-6" />
<T.P className="leading-6 ml-3">
{product.description}
</T.P>
</li>
<li className="grid grid-cols-[24px,1fr] gap-0 text-md items-start mb-2">
<Check className="text-green-600 w-6 h-6" />
<T.P className="leading-6 ml-3">A nice feature</T.P>
</li>
<li className="grid grid-cols-[24px,1fr] gap-0 text-md items-start mb-2">
<Check className="text-green-600 w-6 h-6" />
<T.P className="leading-6 ml-3">
Another nice feature
</T.P>
</li>
<li className="grid grid-cols-[24px,1fr] gap-0 text-md items-start mb-2">
{product.price.unit_amount > 0 ? (
<Check className="text-green-600 w-6 h-6" />
) : (
<X className="text-red-500" />
)}
<T.P className="leading-6 ml-3">
A premium feature
</T.P>
</li>
</ul>
{DIGGER_FEATURES.map((feature, index) => {
const FeatureIcon = feature.icon;
return (
<li key={index} className="grid grid-cols-[24px,1fr] gap-0 text-md items-start mb-2">
<FeatureIcon className="text-green-600 w-6 h-6" />
<div>
<T.P className="leading-6 ml-3 font-semibold">{feature.title}</T.P>
<T.P className="text-sm text-muted-foreground ml-3">{feature.description}</T.P>
</div>
</li>
);
})}
</div>
</div>

Expand Down
122 changes: 122 additions & 0 deletions src/components/FreeTrialDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
'use client'

import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { DIGGER_FEATURES } from '@/constants'
import { getActiveProductsWithPrices } from '@/data/user/organizations'
import { useSAToastMutation } from '@/hooks/useSAToastMutation'
import { createTrialSubSuccessCB } from '@/lib/payments/paymentGatewayUtils'
import { startTrial } from '@/lib/payments/paymentUtilsServer'
import type { UnwrapPromise } from '@/types'
import { useState } from 'react'



function getProductsSortedByPrice(
activeProducts: UnwrapPromise<ReturnType<typeof getActiveProductsWithPrices>>
) {
if (!activeProducts) return []
const products = activeProducts.flatMap((product) => {
const prices = Array.isArray(product.prices) ? product.prices : [product.prices]
return prices.map((price) => ({
...product,
price,
priceString: new Intl.NumberFormat('en-US', {
style: 'currency',
currency: price?.currency ?? undefined,
minimumFractionDigits: 0,
}).format((price?.unit_amount || 0) / 100),
}))
})
return products
.sort((a, b) => (a?.price?.unit_amount ?? 0) - (b?.price?.unit_amount ?? 0))
.filter(Boolean)
}
type FreeTrialDialogProps = {
organizationId: string
activeProducts: UnwrapPromise<ReturnType<typeof getActiveProductsWithPrices>>
isOrganizationAdmin: boolean,
defaultOpen?: boolean
}
export function FreeTrialDialog({ organizationId, activeProducts, isOrganizationAdmin, defaultOpen = false }: FreeTrialDialogProps) {
// this should be true
const [open, setOpen] = useState(defaultOpen)
// supabase cannot sort by foreign table, so we do it here
const productsSortedByPrice = getProductsSortedByPrice(activeProducts);


const { mutate, isLoading } = useSAToastMutation(
async (priceId: string) => {
return await startTrial(organizationId, priceId)
},
{
loadingMessage: 'Starting trial...',
errorMessage: 'Failed to start trial',
successMessage: 'Redirecting to checkout...',
onSuccess(response) {
if (response.status === 'success' && response.data) {
createTrialSubSuccessCB(response.data)
}
},
}
)

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-[900px]">
<DialogHeader>
<DialogTitle>Start Your Free Trial</DialogTitle>
<DialogDescription>
Your organization doesn't have an active subscription. Choose a plan to start your free trial.
</DialogDescription>
</DialogHeader>
<div className="grid grid-cols-1 md:grid-cols-auto gap-4">
{productsSortedByPrice.map((product) => (
<Card key={product.id}>
<CardHeader>
<CardTitle>{product.name}</CardTitle>
<CardDescription>{product.priceString} per {product.price?.interval}</CardDescription>
</CardHeader>
<CardContent>
<ul className="space-y-2">
{DIGGER_FEATURES.map((feature, index) => {
const FeatureIcon = feature.icon
return (
<li key={index} className="flex items-start">
<FeatureIcon className="mr-2 h-4 w-4 mt-1 flex-shrink-0" />
<div>
<span className="font-semibold">{feature.title}</span>
<p className="text-sm text-muted-foreground">{feature.description}</p>
</div>
</li>
)
})}
</ul>
{
isOrganizationAdmin ? (
<Button
className="mt-4 w-full"
onClick={() => mutate(product.price?.id ?? '')}
disabled={isLoading}
>
{isLoading ? 'Starting...' : 'Start Free Trial'}
</Button>
) : (
<Button
className="mt-4 w-full"
disabled
>
Contact your admin
</Button>
)
}
</CardContent>
</Card>
))}
</div>

</DialogContent>
</Dialog>
)
}
45 changes: 45 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Database, FolderIcon, GitPullRequest, Lock, MessageSquare, Terminal, Zap } from "lucide-react";

export const ADMIN_USER_LIST_VIEW_PAGE_SIZE = 10;
export const ADMIN_ORGANIZATION_LIST_VIEW_PAGE_SIZE = 10;
export const PRODUCT_NAME = 'NextBase';
Expand Down Expand Up @@ -45,3 +47,46 @@ export const RESTRICTED_SLUG_NAMES = [
// starts with a letter, ends with a letter or number, and can contain letters, numbers, and hyphens
export const SLUG_PATTERN = /([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})|(^[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9])$/;

type DiggerFeature = {
icon: React.ElementType
title: string
description: string
}

export const DIGGER_FEATURES: DiggerFeature[] = [
{
icon: Lock,
title: "PR level locks",
description: "Digger performs a lock when the PR is opened and unlocks when merged, avoiding stale plan previews."
},
{
icon: FolderIcon,
title: "Dynamic project generation",
description: "Digger traverses directories according to patterns and dynamically generates the list of projects."
},
{
icon: Database,
title: "Plan Persistence",
description: "Store plan outputs in artifacts or cloud provider storage."
},
{
icon: GitPullRequest,
title: "Plan Previews",
description: "Digger runs terraform plan on PR creation and appends output as a comment."
},
{
icon: Zap,
title: "Concurrency",
description: "Independent plan/apply jobs run in parallel."
},
{
icon: MessageSquare,
title: "Real-time updates",
description: "Digger updates a single summary comment with progress in real-time."
},
{
icon: Terminal,
title: "CommentOps",
description: "Use commands like Digger apply, plan, lock, and unlock in comments."
}
]
3 changes: 3 additions & 0 deletions src/utils/server/stripe-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function getIsStripeTestMode() {
return process.env.STRIPE_SECRET_KEY.startsWith('sk_test') || typeof process.env.STRIPE_SECRET_KEY === 'undefined'
}

0 comments on commit cff17fb

Please sign in to comment.