Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
What's done so far:
- Created the new "subscription" view
- Recovered the menu link to this new page
- Show a table with the current subscription. It's been done as a table
  since it looks like it, but I'm not sure if it's useful considering
  there aren't plans for multiple subscriptions
- Removed the "open modal" temporary button from the main menu
- Such modal can now be opened using the "view plans & pricing" button
  in the new page
- A new Subscription Provider/Context has been created, with a
  `permissions` method in order to check for permissions of the current
  plan, but it has not been applied yet anywhere
- The pricing modal has been minimally changed to allow setting a custom
  title, and moving its contents into an independent component
  • Loading branch information
elboletaire committed Nov 19, 2024
1 parent 8b61573 commit f96376c
Show file tree
Hide file tree
Showing 17 changed files with 470 additions and 72 deletions.
5 changes: 4 additions & 1 deletion src/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next'
import { useAccount, useWalletClient, WagmiConfig } from 'wagmi'
import { SaasAccountProvider } from '~components/Account/SaasAccountContext'
import { AuthProvider } from '~components/Auth/AuthContext'
import { SubscriptionProvider } from '~components/Auth/Subscription'
import { walletClientToSigner } from '~constants/wagmi-adapters'
import { VocdoniEnvironment } from './constants'
import { chains, wagmiConfig } from './constants/rainbow'
Expand All @@ -32,7 +33,9 @@ export const Providers = () => {

const SaasProviders = ({ children }: PropsWithChildren<{}>) => (
<AuthProvider>
<SaasAccountProvider>{children}</SaasAccountProvider>
<SubscriptionProvider>
<SaasAccountProvider>{children}</SaasAccountProvider>
</SubscriptionProvider>
</AuthProvider>
)

Expand Down
86 changes: 86 additions & 0 deletions src/components/Auth/Subscription.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { createContext } from '@chakra-ui/react-utils'
import { useQuery } from '@tanstack/react-query'
import { useClient } from '@vocdoni/react-providers'
import { dotobject, ensure0x } from '@vocdoni/sdk'
import { ReactNode, useMemo } from 'react'
import { useAuth } from '~components/Auth/useAuth'
import { ApiEndpoints } from './api'

type PermissionsContextType = {
permission: (key: string) => any
subscription: SubscriptionType
loading: boolean
}

type SubscriptionType = {
subscriptionDetails: {
planID: number
startDate: string // ISO 8601 Date String
endDate: string // ISO 8601 Date String
renewalDate: string // ISO 8601 Date String
active: boolean
maxCensusSize: number
}
usage: {
sentSMS: number
sentEmails: number
subOrgs: number
members: number
}
plan: {
id: number
name: string
stripeID: string
default: boolean
organization: {
memberships: number
subOrgs: number
censusSize: number
}
votingTypes: {
approval: boolean
ranked: boolean
weighted: boolean
}
features: {
personalization: boolean
emailReminder: boolean
smsNotification: boolean
}
}
}

const [SubscriptionProvider, useSubscription] = createContext<PermissionsContextType>({
name: 'PermissionsContext',
errorMessage: 'usePermissions must be used within a PermissionsProvider',
})

const SubscriptionProviderComponent: React.FC<{ children: ReactNode }> = ({ children }) => {
const { bearedFetch } = useAuth()
const { account } = useClient()

// Fetch organization subscription details
// TODO: In the future, this may be merged with the role permissions (not yet defined)
const { data: subscription, isFetching } = useQuery({
queryKey: ['organizationSubscription', account?.address],
queryFn: () =>
bearedFetch<SubscriptionType>(
ApiEndpoints.OrganizationSubscription.replace('{address}', ensure0x(account?.address))
),
// Cache for 15 minutes
staleTime: 15 * 60 * 1000,
enabled: !!account?.address,
})

// Helper function to access permission using dot notation
const permission = useMemo(() => {
return (key: string) => {
if (!subscription || !subscription.plan) return undefined
return dotobject(subscription.plan, key)
}
}, [subscription])

return <SubscriptionProvider value={{ permission, subscription, loading: isFetching }} children={children} />
}

export { SubscriptionProviderComponent as SubscriptionProvider, useSubscription }
3 changes: 2 additions & 1 deletion src/components/Auth/api.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
type MethodTypes = 'GET' | 'POST' | 'PUT' | 'DELETE'

export enum ApiEndpoints {
InviteAccept = 'organizations/{address}/members/accept',
Login = 'auth/login',
Me = 'users/me',
InviteAccept = 'organizations/{address}/members/accept',
Organization = 'organizations/{address}',
OrganizationMembers = 'organizations/{address}/members',
OrganizationPendingMembers = 'organizations/{address}/members/pending',
Organizations = 'organizations',
OrganizationsRoles = 'organizations/roles',
OrganizationSubscription = 'organizations/{address}/subscription',
Password = 'users/password',
PasswordRecovery = 'users/password/recovery',
PasswordReset = 'users/password/reset',
Expand Down
9 changes: 3 additions & 6 deletions src/components/Dashboard/Menu/Options.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { Box, Button, Collapse, useDisclosure } from '@chakra-ui/react'
import { Box, Collapse, useDisclosure } from '@chakra-ui/react'
import { OrganizationName } from '@vocdoni/chakra-components'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { HiSquares2X2 } from 'react-icons/hi2'
import { IoIosSettings } from 'react-icons/io'
import { generatePath, matchPath, useLocation } from 'react-router-dom'
import { Routes } from '~src/router/routes'
import { PricingModal } from '../PricingModal'
import { DashboardMenuItem } from './Item'

type MenuItem = {
Expand Down Expand Up @@ -53,9 +52,9 @@ export const DashboardMenuOptions = () => {
icon: IoIosSettings,
children: [
{ label: t('organization.organization'), route: Routes.dashboard.organization },
{ label: t('team'), route: Routes.dashboard.team },
{ label: t('team.title'), route: Routes.dashboard.team },
// { label: t('billing'), route: '#billing' },
// { label: t('subscription'), route: '#subscription' },
{ label: t('subscription'), route: Routes.dashboard.subscription },
{ label: t('profile'), route: Routes.dashboard.profile },
],
},
Expand Down Expand Up @@ -84,8 +83,6 @@ export const DashboardMenuOptions = () => {

return (
<Box>
<PricingModal isOpenModal={isOpen} onCloseModal={onClose} />
<Button onClick={onOpen}>Open Modal</Button>
<OrganizationName color='text.secondary' mb={2.5} />
{menuItems.map((item, index) => (
<Box key={index}>
Expand Down
94 changes: 52 additions & 42 deletions src/components/Dashboard/PricingModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import {
Select,
Text,
} from '@chakra-ui/react'
import { ReactNode } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { Link as ReactRouterLink } from 'react-router-dom'
import PricingCard from '~components/Organization/Dashboard/PricingCard'
import { useStripePlans } from '~src/queries/stripe'

type CardProps = {
popular: boolean
Expand All @@ -23,9 +25,9 @@ type CardProps = {
features: string[]
}

export const PricingModal = ({ isOpenModal, onCloseModal }: { isOpenModal: boolean; onCloseModal: () => void }) => {
export const PricingContents = () => {
const { t } = useTranslation()

const { data } = useStripePlans()
const cards: CardProps[] = [
{
popular: false,
Expand Down Expand Up @@ -81,46 +83,54 @@ export const PricingModal = ({ isOpenModal, onCloseModal }: { isOpenModal: boole
],
},
]
return (
<Modal isOpen={isOpenModal} onClose={onCloseModal} variant='pricing-modal' size='full'>
<ModalOverlay />
<ModalContent>
<ModalHeader>
<Trans i18nKey='pricing_modal.title'>You need to upgrade to use this feature</Trans>
</ModalHeader>
<ModalCloseButton />
<ModalBody>
{cards.map((card, idx) => (
<PricingCard key={idx} {...card} />
))}
</ModalBody>

<ModalFooter>
<Box>
<Text>
<Trans i18nKey='pricing_modal.more_voters'>If you need more voters, you can select it here:</Trans>
</Text>
<Select>
<option>1-500 members</option>
</Select>
</Box>
return cards.map((card, idx) => <PricingCard key={idx} {...card} />)
}

export const SubscriptionModal = ({
isOpenModal,
onCloseModal,
title,
}: {
isOpenModal: boolean
onCloseModal: () => void
title?: ReactNode
}) => (
<Modal isOpen={isOpenModal} onClose={onCloseModal} variant='pricing-modal' size='full'>
<ModalOverlay />
<ModalContent>
<ModalHeader>
{title || <Trans i18nKey='pricing_modal.upgrade_title'>You need to upgrade to use this feature</Trans>}
</ModalHeader>
<ModalCloseButton />
<ModalBody>
<PricingContents />
</ModalBody>

<ModalFooter>
<Box>
<Text>
<Trans i18nKey='pricing_modal.your_plan'>
Currently you are subscribed to the 'Your plan' subscription. If you upgrade, we will only charge the
yearly difference. In the next billing period, starting on 'dd/mm/yy' you will pay for the new select
plan.
</Trans>
<Trans i18nKey='pricing_modal.more_voters'>If you need more voters, you can select it here:</Trans>
</Text>
<Box>
<Text>
<Trans i18nKey='pricing_modal.help'>Need some help?</Trans>
</Text>
<Button as={ReactRouterLink}>
<Trans i18nKey='contact_us'>Contact us</Trans>
</Button>
</Box>
</ModalFooter>
</ModalContent>
</Modal>
)
}
<Select>
<option>1-500 members</option>
</Select>
</Box>
<Text>
<Trans i18nKey='pricing_modal.your_plan'>
Currently you are subscribed to the 'Your plan' subscription. If you upgrade, we will only charge the yearly
difference. In the next billing period, starting on 'dd/mm/yy' you will pay for the new select plan.
</Trans>
</Text>
<Box>
<Text>
<Trans i18nKey='pricing_modal.help'>Need some help?</Trans>
</Text>
<Button as={ReactRouterLink}>
<Trans i18nKey='contact_us'>Contact us</Trans>
</Button>
</Box>
</ModalFooter>
</ModalContent>
</Modal>
)
18 changes: 15 additions & 3 deletions src/components/Organization/Invite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@ import { FormProvider, useController, useForm, useFormContext } from 'react-hook
import { Trans, useTranslation } from 'react-i18next'
import { ApiEndpoints } from '~components/Auth/api'
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 { SubscriptionPermission } from '~constants'
import { CallbackProvider, useCallbackContext } from '~utils/callback-provider'
import { useTeamMembers } from './Team'

type InviteData = {
email: string
Expand Down Expand Up @@ -178,12 +181,21 @@ const InviteForm = () => {

export const InviteToTeamModal = (props: ButtonProps) => {
const { isOpen, onOpen, onClose } = useDisclosure()
const { permission } = useSubscription()
const { t } = useTranslation()
const { data: members, isLoading } = useTeamMembers()

const canInvite = permission(SubscriptionPermission.Memberships) > (members?.length || 0)

return (
<>
<Button onClick={onOpen} {...props}>
<Trans i18nKey='invite_people'>Invite People</Trans>
</Button>
{canInvite ? (
<Button onClick={onOpen} {...props} isLoading={isLoading} loadingText={t('loading')}>
<Trans i18nKey='invite_people'>Invite People</Trans>
</Button>
) : (
<Text>You must upgrade!</Text>
)}
<CallbackProvider success={() => onClose()}>
<Modal isOpen={isOpen} onClose={onClose} size='xl' closeOnOverlayClick>
<ModalOverlay />
Expand Down
Loading

0 comments on commit f96376c

Please sign in to comment.