diff --git a/src/components/Organization/Invite.tsx b/src/components/Organization/Invite.tsx
index 9b38f0996..8f7d3be56 100644
--- a/src/components/Organization/Invite.tsx
+++ b/src/components/Organization/Invite.tsx
@@ -33,6 +33,7 @@ import { HSeparator } from '~components/Auth/SignIn'
import { useSubscription } from '~components/Auth/Subscription'
import { useAuth } from '~components/Auth/useAuth'
import InputBasic from '~components/Layout/InputBasic'
+import { usePricingModal } from '~components/Pricing/Modals'
import { SubscriptionPermission } from '~constants'
import { CallbackProvider, useCallbackContext } from '~utils/callback-provider'
import { useTeamMembers } from './Team'
@@ -184,18 +185,34 @@ export const InviteToTeamModal = (props: ButtonProps) => {
const { permission } = useSubscription()
const { t } = useTranslation()
const { data: members, isLoading } = useTeamMembers()
+ const { openModal } = usePricingModal()
- const canInvite = permission(SubscriptionPermission.Memberships) > (members?.length || 0)
+ const memberships = permission(SubscriptionPermission.Memberships)
+ const canInvite = memberships > (members?.length || 0)
return (
<>
- {canInvite ? (
-
- ) : (
- You must upgrade!
- )}
+
onClose()}>
diff --git a/src/components/Organization/Subscription.tsx b/src/components/Organization/Subscription.tsx
index 01bcbfc14..b47aaffec 100644
--- a/src/components/Organization/Subscription.tsx
+++ b/src/components/Organization/Subscription.tsx
@@ -22,7 +22,7 @@ export const Subscription = () => {
return (
<>
-
+
diff --git a/src/components/Pricing/Card.tsx b/src/components/Pricing/Card.tsx
index 38b83a1db..68446dc1b 100644
--- a/src/components/Pricing/Card.tsx
+++ b/src/components/Pricing/Card.tsx
@@ -75,7 +75,7 @@ const PricingCard = ({
diff --git a/src/components/Pricing/Modals.tsx b/src/components/Pricing/Modals.tsx
new file mode 100644
index 000000000..d14f3503b
--- /dev/null
+++ b/src/components/Pricing/Modals.tsx
@@ -0,0 +1,52 @@
+import { useDisclosure } from '@chakra-ui/hooks'
+import { createContext } from '@chakra-ui/react-utils'
+import React, { ReactNode, useState } from 'react'
+import { SubscriptionModal } from './Plans'
+import { PlanUpgradeData, PlanUpgradeModal, TierUpgradeModal } from './Upgrading'
+
+// Define types for the context
+type PricingModalType = 'tierUpgrade' | 'planUpgrade' | 'subscription' | null
+
+type PricingModalContextState = {
+ openModal: (type: PricingModalType, modalData?: PlanUpgradeData | null) => void
+ closeModal: () => void
+ modalType: PricingModalType
+ modalData: any
+}
+
+const [PricingModalProviderContext, usePricingModal] = createContext({
+ name: 'PricingModalProvider',
+ errorMessage: 'usePricingModal must be used within a PricingModalProvider',
+ strict: true,
+})
+
+export const PricingModalProvider: React.FC<{ children?: ReactNode }> = ({ children }) => {
+ const { isOpen, onOpen, onClose } = useDisclosure()
+ const [modalType, setModalType] = useState(null)
+ const [modalData, setModalData] = useState(null)
+
+ const openModal = (type: PricingModalType, data?: PlanUpgradeData | null) => {
+ setModalType(type)
+ setModalData(data || null)
+ onOpen()
+ }
+
+ const closeModal = () => {
+ setModalType(null)
+ setModalData(null)
+ onClose()
+ }
+
+ return (
+
+ {children}
+
+ {/* Render modals dynamically based on the modalType */}
+ {modalType === 'tierUpgrade' && isOpen && }
+ {modalType === 'planUpgrade' && isOpen && }
+ {modalType === 'subscription' && isOpen && }
+
+ )
+}
+
+export { usePricingModal }
diff --git a/src/components/Pricing/Plans.tsx b/src/components/Pricing/Plans.tsx
index 912c2550b..2ab90c379 100644
--- a/src/components/Pricing/Plans.tsx
+++ b/src/components/Pricing/Plans.tsx
@@ -20,7 +20,7 @@ import { Link as ReactRouterLink } from 'react-router-dom'
import { ApiEndpoints } from '~components/Auth/api'
import { useSubscription } from '~components/Auth/Subscription'
import { useAuth } from '~components/Auth/useAuth'
-import { StripeId } from '~constants'
+import { PlanId } from '~constants'
import PricingCard from './Card'
export type Plan = {
@@ -64,15 +64,10 @@ export const usePlans = () => {
})
}
-export const SubscriptionPlans = () => {
+export const usePlanTranslations = () => {
const { t } = useTranslation()
- const { data: plans, isLoading } = usePlans()
- const { permission } = useSubscription()
-
- const [selectedCensusSize, setSelectedCensusSize] = useState(null)
-
const translations = {
- [StripeId.Free]: {
+ [PlanId.Free]: {
title: t('pricing.free_title', { defaultValue: 'Free' }),
subtitle: t('pricing.free_subtitle', {
defaultValue: 'Small organizations or community groups with basic voting needs.',
@@ -90,7 +85,7 @@ export const SubscriptionPlans = () => {
t('pricing.gpdr_compilance', { defaultValue: 'GDPR compliance' }),
],
},
- [StripeId.Essential]: {
+ [PlanId.Essential]: {
title: t('pricing.essential_title', { defaultValue: 'Essential' }),
subtitle: t('pricing.essential_subtitle', {
defaultValue: 'Small or medium-sized orgs or community groups with basic voting needs.',
@@ -104,7 +99,7 @@ export const SubscriptionPlans = () => {
t('pricing.gpdr_compilance'),
],
},
- [StripeId.Premium]: {
+ [PlanId.Premium]: {
title: t('pricing.premium_title', { defaultValue: 'Premium' }),
subtitle: t('pricing.premium_subtitle', {
defaultValue: 'Larger amount that require more advanced features.',
@@ -118,30 +113,69 @@ export const SubscriptionPlans = () => {
t('pricing.gpdr_compilance'),
],
},
+ [PlanId.Custom]: {
+ title: t('pricing.custom_title', { defaultValue: 'Custom' }),
+ subtitle: t('pricing.custom_subtitle', {
+ defaultValue:
+ 'Large organizations, enterprises, and institutions requiring extensive customization and support',
+ }),
+ features: [
+ t('pricing.all_features', { defaultValue: 'All features & voting types' }),
+ t('pricing.up_to_admins', { admin: 10, org: 5 }),
+ t('pricing.unlimited_yearly_processes', { defaultValue: 'Unlimited yearly voting processes' }),
+ t('pricing.white_label', { defaultValue: 'White label solution' }),
+ t('pricing.advanced_analytitcs', { defaultValue: 'Advanced reporting and analytics' }),
+ t('pricing.dedicated_manager', { defaultValue: 'Dedicated account manager' }),
+ t('pricing.priority_support', { defaultValue: 'Priority ticket support' }),
+ t('pricing.gpdr_compilance'),
+ ],
+ },
}
+ return translations
+}
+
+export const SubscriptionPlans = () => {
+ const { t } = useTranslation()
+ const { data: plans, isLoading } = usePlans()
+ const { permission } = useSubscription()
+ const translations = usePlanTranslations()
+
+ const [selectedCensusSize, setSelectedCensusSize] = useState(null)
+
const censusSizeOptions = useMemo(() => {
- const allTiers = plans
- // Exclude plans with null censusSizeTiers
- ?.filter((plan) => plan.censusSizeTiers)
- .flatMap((plan) =>
- plan.censusSizeTiers!.map((tier) => {
- const from = tier.upTo === 100 ? 1 : tier.upTo - 99
- return {
- label: t('pricing.members_size', { defaultValue: '{{ from }}-{{ to }} members', from, to: tier.upTo }),
- value: tier.upTo,
- }
- })
- )
- const uniqueTiers = Array.from(new Map(allTiers?.map((tier) => [tier.value, tier])).values())
- return uniqueTiers || []
- }, [plans])
+ if (!plans) return []
+
+ // Step 1: Merge censusSizeTiers from all plans, removing duplicates
+ const mergedTiers = plans
+ .flatMap((plan) => plan.censusSizeTiers || []) // Combine all tiers
+ .reduce((acc, tier) => {
+ if (!acc.has(tier.upTo)) {
+ acc.set(tier.upTo, tier) // Keep unique `upTo` values
+ }
+ return acc
+ }, new Map())
+
+ // Step 2: Create options array from merged and sorted tiers
+ const sortedTiers = Array.from(mergedTiers.values()).sort((a, b) => a.upTo - b.upTo)
+
+ const options = sortedTiers.map((tier, idx) => {
+ const previous = sortedTiers[idx - 1] || { upTo: 0 }
+ const from = previous.upTo + 1
+ return {
+ label: t('pricing.members_size', { defaultValue: '{{ from }}-{{ to }} members', from, to: tier.upTo }),
+ value: tier.upTo,
+ }
+ })
+
+ return options
+ }, [plans, t])
const cards = useMemo(() => {
if (!plans) return []
return plans.map((plan) => ({
- popular: plan.default,
+ popular: plan.id === PlanId.Essential,
title: translations[plan.id]?.title || plan.name,
subtitle: translations[plan.id]?.subtitle || '',
price: plan.startingPrice / 100,
@@ -159,7 +193,7 @@ export const SubscriptionPlans = () => {