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 = () => {