Skip to content

Commit

Permalink
integrating process features with plan permissions
Browse files Browse the repository at this point in the history
- Trying to get rid of the plans mock, but it still needs some work
  (complicated when most features are missing from the plan rn)
- Commented all the non implemented features in the process create form
  • Loading branch information
elboletaire committed Nov 28, 2024
1 parent 00d43e8 commit 41e3239
Show file tree
Hide file tree
Showing 14 changed files with 244 additions and 379 deletions.
74 changes: 0 additions & 74 deletions src/components/Account/useAccountPlan.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion src/components/Dashboard/ProcessView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ export const ProcessView = () => {
{/* Features Section */}
<DashboardBox textAlign='center' display='flex' flexDir='column' gap={3}>
<Heading as='h4' variant='sidebar-section'>
<Trans i18nKey='features'>Features</Trans>
<Trans i18nKey='features.title'>Features</Trans>
</Heading>
<HStack spacing={3} justifyContent='center'>
<Text fontSize='sm'>Feature 1</Text>
Expand Down
33 changes: 3 additions & 30 deletions src/components/Pricing/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
useDisclosure,
} from '@chakra-ui/react'
import { dotobject } from '@vocdoni/sdk'
import { Trans, useTranslation } from 'react-i18next'
import { Trans } from 'react-i18next'
import { PlanFeaturesTranslationKeys } from './Features'
import type { Plan } from './Plans'

type PricingCardProps = {
Expand All @@ -29,18 +30,6 @@ type PricingCardProps = {
plan: Plan
}

// Translation keys for the features
const translations = {
'organization.memberships': 'pricing.features.memberships',
'organization.subOrgs': 'pricing.features.sub_orgs',
'votingTypes.weighted': 'pricing.features.weighted',
'votingTypes.approval': 'pricing.features.approval',
'votingTypes.ranked': 'pricing.features.ranked',
'features.personalization': 'pricing.features.personalization',
'features.emailReminder': 'pricing.features.email_reminder',
'features.smsNotification': 'pricing.features.sms_notification',
}

const PricingCard = ({
title,
subtitle,
Expand All @@ -54,7 +43,7 @@ const PricingCard = ({
const { isOpen, onToggle } = useDisclosure()

// Dynamically map the features from the plan
const features = Object.entries(translations)
const features = Object.entries(PlanFeaturesTranslationKeys)
.map(([key, translationKey]) => {
const value = dotobject(plan, key)
return value !== undefined
Expand Down Expand Up @@ -138,20 +127,4 @@ const PricingCard = ({
)
}

// yeah, it's sad but we need to include all the translations in a way the extractor does not remove them...
// note this component does not need (and should never) to be included in the app
const UnusedComponentButRequiredToNotLoseTranslations = () => {
const { t } = useTranslation()
t('pricing.features.memberships', { defaultValue: 'Up to {{ count }} memberships' })
t('pricing.features.sub_orgs', { defaultValue: 'Up to {{ count }} sub-organizations' })
t('pricing.features.approval', { defaultValue: 'Approval voting' })
t('pricing.features.ranked', { defaultValue: 'Ranked voting' })
t('pricing.features.weighted', { defaultValue: 'Weighted voting' })
t('pricing.features.personalization', { defaultValue: 'Personalization' })
t('pricing.features.email_reminder', { defaultValue: 'Email reminder' })
t('pricing.features.sms_notification', { defaultValue: 'SMS notification' })

return null
}

export default PricingCard
94 changes: 94 additions & 0 deletions src/components/Pricing/Features.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { dotobject } from '@vocdoni/sdk'
import { useTranslation } from 'react-i18next'
import type { Plan } from './Plans'

export type FeaturesKeys =
| 'anonymous'
| 'secretUntilTheEnd'
| 'overwrite'
| 'personalization'
| 'emailReminder'
| 'smsNotification'
| 'whiteLabel'
| 'liveStreaming'

// Translation keys for the subscription features
export const PlanFeaturesTranslationKeys = {
'organization.memberships': 'features.memberships',
'organization.subOrgs': 'features.sub_orgs',
'votingTypes.weighted': 'features.weighted',
'votingTypes.approval': 'features.approval',
'votingTypes.ranked': 'features.ranked',
'features.personalization': 'features.personalization',
'features.emailReminder': 'features.email_reminder',
'features.smsNotification': 'features.sms_notification',
}

/**
* Checks if a given feature exists and meets the required condition in a plan.
*
* @param plan - The plan object to check.
* @param featurePath - Dot notation path to the feature (e.g., 'organization.memberships').
* @param expectedValue - Expected value or comparison object.
* - If a number, checks for >= comparison.
* - If an object, supports { operator, value } (e.g., { operator: '>=', value: 10 }).
* @returns boolean - `true` if the feature meets the condition, `false` otherwise.
*/
export const isFeatureAvailable = (
plan: Plan,
featurePath: string,
expectedValue?: number | { operator: '===' | '>' | '>=' | '<' | '<='; value: number }
): boolean => {
const featureValue = dotobject(plan, featurePath) // Get the feature value using dot notation

if (typeof featureValue === 'undefined') {
return false // If the feature doesn't exist, return false
}

// If no expected value is provided, return true if the feature exists
if (typeof expectedValue === 'undefined') {
return true
}

// Handle exact match or comparison
if (typeof expectedValue === 'number') {
return featureValue >= expectedValue // Default to "greater than or equal to" for numbers
}

if (typeof expectedValue === 'object' && expectedValue.operator && expectedValue.value !== undefined) {
const { operator, value } = expectedValue

switch (operator) {
case '===':
return featureValue === value
case '>':
return featureValue > value
case '>=':
return featureValue >= value
case '<':
return featureValue < value
case '<=':
return featureValue <= value
default:
throw new Error(`Unsupported operator: ${operator}`)
}
}

throw new Error('Invalid expectedValue type')
}

// yeah, it's sad but we need to include all the translations in a way the extractor does not remove them...
// note this component does not need (and should never) to be included in the app
const UnusedComponentButRequiredToNotLoseTranslations = () => {
const { t } = useTranslation()
t('features.memberships', { defaultValue: 'Up to {{ count }} memberships' })
t('features.sub_orgs', { defaultValue: 'Up to {{ count }} sub-organizations' })
t('features.approval', { defaultValue: 'Approval voting' })
t('features.ranked', { defaultValue: 'Ranked voting' })
t('features.weighted', { defaultValue: 'Weighted voting' })
t('features.personalization', { defaultValue: 'Personalization' })
t('features.email_reminder', { defaultValue: 'Email reminder' })
t('features.sms_notification', { defaultValue: 'SMS notification' })

return null
}
9 changes: 4 additions & 5 deletions src/components/Pricing/Upgrading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,18 +109,17 @@ export const PlanUpgrade = ({ feature, text, value }: PlanUpgradeData) => {
</Text>
<Flex justify='space-evenly' fontSize='sm' fontWeight='semibold' height={6}>
{isLoading && <Spinner />}
{plans?.map((plan) => (
<Fragment key={plan.id}>
{plans?.map((plan, key) => (
<Fragment key={key}>
<Flex align='center' gap={1}>
{(!value && isFeatureAvailable(plan, feature)) ||
(value && isFeatureAvailable(plan, feature, value)) ? (
{isFeatureAvailable(plan, feature, value) ? (
<Icon as={CheckCircle} color='green.500' fontSize='xl' />
) : (
<Icon as={XCircle} color='red.500' fontSize='xl' />
)}
<Text>{translations[plan.id].title}</Text>
</Flex>
<Divider orientation='vertical' />
{key < plans.length - 1 && <Divider orientation='vertical' />}
</Fragment>
))}
</Flex>
Expand Down
82 changes: 0 additions & 82 deletions src/components/ProcessCreate/Questions/useSaasVotingType.ts

This file was deleted.

24 changes: 7 additions & 17 deletions src/components/ProcessCreate/SaasFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { Box, Flex, Link, Text } from '@chakra-ui/react'
import { Button } from '@vocdoni/chakra-components'
import { Box, Flex, Link } from '@chakra-ui/react'
import { useTranslation } from 'react-i18next'
import { Link as ReactRouterLink } from 'react-router-dom'
import { useAccountPlan } from '~components/Account/useAccountPlan'
import { useSubscription } from '~components/Auth/Subscription'
import { VocdoniLogo } from '~components/Layout/Logo'
import { PlanId } from '~constants'

const SaasFooter = () => {
const { t } = useTranslation()
const { data } = useAccountPlan()
const isCustom = data?.plan === 'custom'
const isFree = data?.plan === 'free'
const { subscription } = useSubscription()
const isCustom = subscription?.plan.id === PlanId.Custom
const isFree = subscription?.plan.id === PlanId.Free

return (
<Box bgColor={'process_create.bg_light'} _dark={{ bgColor: 'process_create.bg_dark' }}>
Expand All @@ -35,17 +35,7 @@ const SaasFooter = () => {
<Link as={ReactRouterLink} to=''>
{t('privacy_policy', { defaultValue: 'Privacy Policy' })}
</Link>
<Link as={ReactRouterLink} to=''>
[email protected]
</Link>
</Flex>
<Flex flexDirection={{ base: 'column', lg: 'row' }} alignItems='center' gap={6} order={{ base: 1, lg: 2 }}>
{isFree && (
<Text fontWeight='bold' fontSize='xl' mb={0}>
$0.00
</Text>
)}
{!isCustom && <Button>UPGRADE TO PREMIUM</Button>}
<Link href='mailto:[email protected]'>[email protected]</Link>
</Flex>
</Flex>
</Box>
Expand Down
Loading

0 comments on commit 41e3239

Please sign in to comment.