diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/@sidebar/OrganizationSidebar.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/@sidebar/OrganizationSidebar.tsx index 2bc96ffc..953a573a 100644 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/@sidebar/OrganizationSidebar.tsx +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/@sidebar/OrganizationSidebar.tsx @@ -27,6 +27,8 @@ async function OrganizationSubscriptionSidebarCard({ const isOrganizationAdmin = userRole === 'admin' || userRole === 'owner' switch (normalizedSubscription.type) { + case 'bypassed_enterprise_organization': + return null; case 'trialing': return + + + Subscription + + + This organization is using the enterprise (demo) plan. Contact application administrator to modify subscription details. + + + ; + } + if ( !subscriptionDetails.title || normalizedSubscription.type === 'no-subscription' diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/ApprovalControls.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/ApprovalControls.tsx index f24a2ca4..feb1742f 100644 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/ApprovalControls.tsx +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/project/[projectSlug]/(specific-project-pages)/ApprovalControls.tsx @@ -2,8 +2,7 @@ import { getLoggedInUserOrganizationRole, - getNormalizedOrganizationSubscription, - getSlimOrganizationById, + getSlimOrganizationById } from '@/data/user/organizations'; import { getSlimProjectById } from '@/data/user/projects'; import { getLoggedInUserTeamRole, getSlimTeamById } from '@/data/user/teams'; @@ -19,7 +18,6 @@ async function fetchData(projectId: string) { projectByIdData.team_id ? getLoggedInUserTeamRole(projectByIdData.team_id) : null, - getNormalizedOrganizationSubscription(projectByIdData.organization_id), ]); return { diff --git a/src/components/SubscriptionCardSmall/SubscriptionCardSmall.tsx b/src/components/SubscriptionCardSmall/SubscriptionCardSmall.tsx deleted file mode 100644 index 484f0d02..00000000 --- a/src/components/SubscriptionCardSmall/SubscriptionCardSmall.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { T } from '@/components/ui/Typography'; -import { Button } from '@/components/ui/button'; -import { - HoverCard, - HoverCardContent, - HoverCardTrigger, -} from '@/components/ui/hover-card'; -import { getNormalizedOrganizationSubscription } from '@/data/user/organizations'; -import { formatNormalizedSubscription } from '@/utils/formatNormalizedSubscription'; -import { ArrowUpRight } from 'lucide-react'; -import Link from 'next/link'; -import { Card } from '../ui/card'; - -export async function SubscriptionCardSmall({ - organizationId, - organizationSlug, -}: { - organizationId: string; - organizationSlug: string; -}) { - const normalizedSubscription = - await getNormalizedOrganizationSubscription(organizationId); - - - const { title, sidenote, description } = formatNormalizedSubscription( - normalizedSubscription, - ); - - if (title) { - return ( - - - -
- {title} Pro - {sidenote ? ( - - {sidenote} - - ) : null} -
- -
- {description} -
- ); - } - return ( - -

{description}

- - - -
- ); -} diff --git a/src/components/SubscriptionCardSmall/index.ts b/src/components/SubscriptionCardSmall/index.ts deleted file mode 100644 index 7ba489f7..00000000 --- a/src/components/SubscriptionCardSmall/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './SubscriptionCardSmall'; diff --git a/src/data/user/organizations.ts b/src/data/user/organizations.ts index 257c08e8..dfbcb2fc 100644 --- a/src/data/user/organizations.ts +++ b/src/data/user/organizations.ts @@ -313,11 +313,34 @@ export const getNormalizedOrganizationSubscription = async ( organizationId: string, ): Promise => { const supabase = createSupabaseUserServerComponentClient(); - const { data: subscriptions, error } = await supabase - .from('subscriptions') - .select('*, prices(*, products(*))') - .eq('organization_id', organizationId) - .in('status', ['trialing', 'active']); + const [organizationSubscriptionsResponse, byOrganizationsResponse] = + await Promise.all([ + supabase + .from('subscriptions') + .select('*, prices(*, products(*))') + .eq('organization_id', organizationId) + .in('status', ['trialing', 'active']), + supabase + .from('billing_bypass_organizations') + .select('*') + .eq('id', organizationId) + .single(), + ]); + + const { data: bypassOrganizations, error: bypassOrganizationsError } = + byOrganizationsResponse; + + if (bypassOrganizationsError) { + // ignore this is the likely case. + } + + if (bypassOrganizations) { + return { + type: 'bypassed_enterprise_organization', + }; + } + + const { data: subscriptions, error } = organizationSubscriptionsResponse; if (error) { throw error; @@ -385,6 +408,7 @@ export const getActiveProductsWithPrices = async () => { .select('*, prices(*)') .eq('active', true) .eq('prices.active', true) + .eq('is_visible_in_ui', true) .order('unit_amount', { foreignTable: 'prices' }); if (error) { diff --git a/src/lib/database.types.ts b/src/lib/database.types.ts index 4491bd90..1b3723a2 100644 --- a/src/lib/database.types.ts +++ b/src/lib/database.types.ts @@ -32,6 +32,29 @@ export type Database = { }, ] } + billing_bypass_organizations: { + Row: { + created_at: string + id: string + } + Insert: { + created_at?: string + id: string + } + Update: { + created_at?: string + id?: string + } + Relationships: [ + { + foreignKeyName: "billing_bypass_organizations_id_fkey" + columns: ["id"] + isOneToOne: true + referencedRelation: "organizations" + referencedColumns: ["id"] + }, + ] + } chats: { Row: { created_at: string @@ -1193,6 +1216,7 @@ export type Database = { description: string | null id: string image: string | null + is_visible_in_ui: boolean metadata: Json | null name: string | null } @@ -1201,6 +1225,7 @@ export type Database = { description?: string | null id: string image?: string | null + is_visible_in_ui?: boolean metadata?: Json | null name?: string | null } @@ -1209,6 +1234,7 @@ export type Database = { description?: string | null id?: string image?: string | null + is_visible_in_ui?: boolean metadata?: Json | null name?: string | null } diff --git a/src/types.ts b/src/types.ts index af439ea9..6764e85c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,24 +1,24 @@ -import type { SupabaseClient } from "@supabase/supabase-js"; -import type { CoreMessage } from "ai"; -import type { Database } from "./lib/database.types"; +import type { SupabaseClient } from '@supabase/supabase-js'; +import type { CoreMessage } from 'ai'; +import type { Database } from './lib/database.types'; export type AppSupabaseClient = SupabaseClient; -export type Table = - Database["public"]["Tables"][T]["Row"]; -export type TableInsertPayload = - Database["public"]["Tables"][T]["Insert"]; -export type TableUpdatePayload = - Database["public"]["Tables"][T]["Update"]; - -export type View = - Database["public"]["Views"][T]["Row"]; -export type DBFunction = - Database["public"]["Functions"][T]["Returns"]; +export type Table = + Database['public']['Tables'][T]['Row']; +export type TableInsertPayload = + Database['public']['Tables'][T]['Insert']; +export type TableUpdatePayload = + Database['public']['Tables'][T]['Update']; + +export type View = + Database['public']['Views'][T]['Row']; +export type DBFunction = + Database['public']['Functions'][T]['Returns']; export type UnwrapPromise = T extends Promise ? U : T; -export type Enum = - Database["public"]["Enums"][T]; +export type Enum = + Database['public']['Enums'][T]; export interface SupabaseFileUploadOptions { /** @@ -37,22 +37,22 @@ export interface SupabaseFileUploadOptions { /** One of the providers supported by GoTrue. */ export type AuthProvider = - | "apple" - | "azure" - | "bitbucket" - | "discord" - | "facebook" - | "github" - | "gitlab" - | "google" - | "keycloak" - | "linkedin" - | "notion" - | "slack" - | "spotify" - | "twitch" - | "twitter" - | "workos"; + | 'apple' + | 'azure' + | 'bitbucket' + | 'discord' + | 'facebook' + | 'github' + | 'gitlab' + | 'google' + | 'keycloak' + | 'linkedin' + | 'notion' + | 'slack' + | 'spotify' + | 'twitch' + | 'twitter' + | 'workos'; export type DropzoneFile = File & { path: string; @@ -62,70 +62,74 @@ export type DropzoneFileWithDuration = DropzoneFile & { duration: number; }; -export type CommentWithUser = Table<"project_comments"> & { - user_profile: Table<"user_profiles">; +export type CommentWithUser = Table<'project_comments'> & { + user_profile: Table<'user_profiles'>; }; export type NoSubscription = { - type: "no-subscription"; + type: 'no-subscription'; }; export type TrialSubscription = { - type: "trialing"; + type: 'trialing'; trialStart: string; trialEnd: string; - product: Table<"products">; - price: Table<"prices">; - subscription: Table<"subscriptions">; + product: Table<'products'>; + price: Table<'prices'>; + subscription: Table<'subscriptions'>; }; export type ActiveSubscription = { - type: "active"; - product: Table<"products">; - price: Table<"prices">; - subscription: Table<"subscriptions">; + type: 'active'; + product: Table<'products'>; + price: Table<'prices'>; + subscription: Table<'subscriptions'>; }; export type PastDueSubscription = { - type: "past_due"; - product: Table<"products">; - price: Table<"prices">; - subscription: Table<"subscriptions">; + type: 'past_due'; + product: Table<'products'>; + price: Table<'prices'>; + subscription: Table<'subscriptions'>; }; export type CanceledSubscription = { - type: "canceled"; - product: Table<"products">; - price: Table<"prices">; - subscription: Table<"subscriptions">; + type: 'canceled'; + product: Table<'products'>; + price: Table<'prices'>; + subscription: Table<'subscriptions'>; }; export type PausedSubscription = { - type: "paused"; - product: Table<"products">; - price: Table<"prices">; - subscription: Table<"subscriptions">; + type: 'paused'; + product: Table<'products'>; + price: Table<'prices'>; + subscription: Table<'subscriptions'>; }; export type IncompleteSubscription = { - type: "incomplete"; - product: Table<"products">; - price: Table<"prices">; - subscription: Table<"subscriptions">; + type: 'incomplete'; + product: Table<'products'>; + price: Table<'prices'>; + subscription: Table<'subscriptions'>; }; export type IncompleteExpiredSubscription = { - type: "incomplete_expired"; - product: Table<"products">; - price: Table<"prices">; - subscription: Table<"subscriptions">; + type: 'incomplete_expired'; + product: Table<'products'>; + price: Table<'prices'>; + subscription: Table<'subscriptions'>; }; export type UnpaidSubscription = { - type: "unpaid"; - product: Table<"products">; - price: Table<"prices">; - subscription: Table<"subscriptions">; + type: 'unpaid'; + product: Table<'products'>; + price: Table<'prices'>; + subscription: Table<'subscriptions'>; +}; + +export type BypassedEnterpriseOrganization = { + type: 'bypassed_enterprise_organization'; }; export type NormalizedSubscription = @@ -137,7 +141,8 @@ export type NormalizedSubscription = | PausedSubscription | IncompleteSubscription | IncompleteExpiredSubscription - | UnpaidSubscription; + | UnpaidSubscription + | BypassedEnterpriseOrganization; export type TeamMemberRowProps = { name?: string; @@ -154,19 +159,20 @@ export type TeamMembersTableProps = { }; export type SASuccessPayload = { - status: "success"; + status: 'success'; } & (TData extends undefined ? { data?: TData } : { data: TData }); export type SAErrorPayload = { - status: "error"; + status: 'error'; message: string; }; /** * Server Action Payload */ -export type SAPayload = SASuccessPayload | SAErrorPayload; - +export type SAPayload = + | SASuccessPayload + | SAErrorPayload; export type Message = CoreMessage & { id: string; diff --git a/src/utils/formatNormalizedSubscription.ts b/src/utils/formatNormalizedSubscription.ts index 5467b416..858d1913 100644 --- a/src/utils/formatNormalizedSubscription.ts +++ b/src/utils/formatNormalizedSubscription.ts @@ -14,6 +14,13 @@ export function formatNormalizedSubscription( const threeDaysFromNow = moment().add(3, 'days'); let description = ''; switch (subscription.type) { + case 'bypassed_enterprise_organization': + return { + title: 'Enterprise (Demo) Plan', + sidenote: '', + description: + 'This organization is using the enterprise (demo) plan. Contact application administrator to modify subscription details.', + }; case 'no-subscription': return { title: '', diff --git a/supabase/migrations/20240820162919_custom_pricing.sql b/supabase/migrations/20240820162919_custom_pricing.sql new file mode 100644 index 00000000..56101dbb --- /dev/null +++ b/supabase/migrations/20240820162919_custom_pricing.sql @@ -0,0 +1,24 @@ +ALTER TABLE "public"."products" +ADD COLUMN "is_visible_in_ui" boolean NOT NULL DEFAULT false; + +-- Create the billing_bypass_organizations table +CREATE TABLE public.billing_bypass_organizations ( + id UUID PRIMARY KEY REFERENCES public.organizations(id) ON DELETE CASCADE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL +); + +-- Enable Row Level Security +ALTER TABLE public.billing_bypass_organizations ENABLE ROW LEVEL SECURITY; + +-- Policy for organization members to view their own organization +CREATE POLICY "Organization members can view their own enterprise organization" +ON public.billing_bypass_organizations +FOR SELECT +USING ( + auth.uid() IN ( + SELECT member_id + FROM public.organization_members + WHERE organization_id = billing_bypass_organizations.id + ) +); +