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 dc73a2d
Show file tree
Hide file tree
Showing 13 changed files with 406 additions and 55 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.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
7 changes: 2 additions & 5 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 @@ -55,7 +54,7 @@ export const DashboardMenuOptions = () => {
{ label: t('organization.organization'), route: Routes.dashboard.organization },
{ label: t('team'), 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>
)
91 changes: 91 additions & 0 deletions src/components/Organization/Subscription.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {
Avatar,
Button,
Progress,
Table,
TableContainer,
Tag,
Tbody,
Td,
Th,
Thead,
Tr,
useDisclosure,
} from '@chakra-ui/react'
import { Trans, useTranslation } from 'react-i18next'
import { useSubscription } from '~components/Auth/Subscription'
import { SubscriptionModal } from '~components/Dashboard/PricingModal'

export const Subscription = () => {
const { t } = useTranslation()
const { isOpen, onClose, onOpen } = useDisclosure()

return (
<>
<SubscriptionModal isOpenModal={isOpen} onCloseModal={onClose} title={t('pricing_modal.title')} />
<Button onClick={onOpen} alignSelf='end'>
View Plans & Pricing
</Button>
<SubscriptionList />
</>
)
}

export const SubscriptionList = () => {
const { subscription, loading } = useSubscription()

if (loading) {
return <Progress size='xs' isIndeterminate />
}

if (!subscription) {
return null
}

return (
<TableContainer>
<Table size='sm'>
<Thead>
<Tr>
<Th>
<Trans i18nKey='subscription.your_subscription'>Your Subscription</Trans>
</Th>
<Th>
<Trans i18nKey='subscription.price'>Price</Trans>
</Th>
<Th>
<Trans i18nKey='subscription.since'>Since</Trans>
</Th>
<Th colSpan={2}>
<Trans i18nKey='subscription.next_billing'>Next Billing</Trans>
</Th>
</Tr>
</Thead>
<Tbody>
<Tr>
<Td display='flex' alignItems='center' gap={3}>
<Avatar name={subscription.plan.name} size='sm' />
{subscription.plan.name} ({subscription.plan.organization.memberships} members)
</Td>
<Td>
<Tag>undefined</Tag>
</Td>
<Td>
<Tag>{new Date(subscription.subscriptionDetails.startDate).toLocaleDateString()}</Tag>
</Td>
<Td>
<Tag>{new Date(subscription.subscriptionDetails.renewalDate).toLocaleDateString()}</Tag>
</Td>
<Td>
<Button variant='outline' size='sm'>
<Trans i18nKey='subscription.change_plan_button'>Change</Trans>
</Button>
</Td>
</Tr>
</Tbody>
</Table>
</TableContainer>
)
}

export const SubscriptionHistory = () => {}
23 changes: 23 additions & 0 deletions src/elements/dashboard/subscription.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useOutletContext } from 'react-router-dom'
import { DashboardContents } from '~components/Layout/Dashboard'
import { Subscription } from '~components/Organization/Subscription'
import { DashboardLayoutContext } from '~elements/LayoutDashboard'

const SubscriptionPage = () => {
const { t } = useTranslation()
const { setTitle } = useOutletContext<DashboardLayoutContext>()

useEffect(() => {
setTitle(t('subscription', { defaultValue: 'Subscription' }))
}, [])

return (
<DashboardContents display='flex' flexDir='column'>
<Subscription />
</DashboardContents>
)
}

export default SubscriptionPage
Loading

0 comments on commit dc73a2d

Please sign in to comment.