diff --git a/src/App.tsx b/src/App.tsx index 32c7f7da43..91e74d0360 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -335,6 +335,7 @@ export default function App() { const isSidebarHidden = location?.pathname.includes(APP_PATHS.PointsVault) || location?.pathname.includes('/snap') || + location?.pathname.includes('/pricing') || location?.pathname.includes(APP_PATHS.DiscordVerification); useInAppNotifications(); diff --git a/src/blocks/icons/components/Meteor.tsx b/src/blocks/icons/components/Meteor.tsx new file mode 100644 index 0000000000..d58f3342af --- /dev/null +++ b/src/blocks/icons/components/Meteor.tsx @@ -0,0 +1,73 @@ +import { FC } from 'react'; +import { IconWrapper } from '../IconWrapper'; +import { IconProps } from '../Icons.types'; + +const Meteor: FC = (allProps) => { + const { svgProps: props, ...restProps } = allProps; + return ( + + + + + + + + + + + + + + + + } + {...restProps} + /> + ); +}; + +export default Meteor; diff --git a/src/blocks/icons/index.ts b/src/blocks/icons/index.ts index 9b49da2f6b..07e93a0dc7 100644 --- a/src/blocks/icons/index.ts +++ b/src/blocks/icons/index.ts @@ -103,6 +103,8 @@ export { default as MaximizeLeft } from './components/MaximizeLeft'; export { default as MegaPhone } from './components/MegaPhone'; +export { default as Meteor } from './components/Meteor'; + export { default as Moon } from './components/Moon'; export { default as MoonFilled } from './components/MoonFilled'; diff --git a/src/blocks/illustrations/components/PushLogo.tsx b/src/blocks/illustrations/components/PushLogo.tsx index 53ac3f2698..144b4767dc 100644 --- a/src/blocks/illustrations/components/PushLogo.tsx +++ b/src/blocks/illustrations/components/PushLogo.tsx @@ -6,7 +6,7 @@ const PushLogo: FC = (allProps) => { const { svgProps: props, ...restProps } = allProps; return ( = (allProps) => { gradientUnits="userSpaceOnUse" > - - - - - - - + + + + + + + = (allProps) => { gradientUnits="userSpaceOnUse" > - - - - - - - + + + + + + + = (allProps) => { gradientUnits="userSpaceOnUse" > - - - - - - - + + + + + + + = (allProps) => { gradientUnits="userSpaceOnUse" > - - - - - - - + + + + + + + = (allProps) => { gradientUnits="userSpaceOnUse" > - - - - - - - + + + + + + + = (allProps) => { gradientUnits="userSpaceOnUse" > - - - - - - - + + + + + + + diff --git a/src/blocks/illustrations/components/PushLogoWithNameDark.tsx b/src/blocks/illustrations/components/PushLogoWithNameDark.tsx new file mode 100644 index 0000000000..4b98dfe999 --- /dev/null +++ b/src/blocks/illustrations/components/PushLogoWithNameDark.tsx @@ -0,0 +1,296 @@ +import { FC } from 'react'; +import { IllustrationWrapper } from '../IllustrationWrapper'; +import { IllustrationProps } from '../Illustrations.types'; + +const PushLogoWithNameDark: FC = (allProps) => { + const { svgProps: props, ...restProps } = allProps; + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + } + {...restProps} + /> + ); +}; + +export default PushLogoWithNameDark; diff --git a/src/blocks/illustrations/components/PushLogoWithNameLight.tsx b/src/blocks/illustrations/components/PushLogoWithNameLight.tsx new file mode 100644 index 0000000000..620a0c3ecd --- /dev/null +++ b/src/blocks/illustrations/components/PushLogoWithNameLight.tsx @@ -0,0 +1,296 @@ +import { FC } from 'react'; +import { IllustrationWrapper } from '../IllustrationWrapper'; +import { IllustrationProps } from '../Illustrations.types'; + +const PushLogoWithNameLight: FC = (allProps) => { + const { svgProps: props, ...restProps } = allProps; + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + } + {...restProps} + /> + ); +}; + +export default PushLogoWithNameLight; diff --git a/src/blocks/illustrations/index.ts b/src/blocks/illustrations/index.ts index 5d9470392a..e3562b51ad 100644 --- a/src/blocks/illustrations/index.ts +++ b/src/blocks/illustrations/index.ts @@ -109,6 +109,10 @@ export { default as RewardsCoin } from './components/RewardsCoin'; export { default as PushLogo } from './components/PushLogo'; +export { default as PushLogoWithNameLight } from './components/PushLogoWithNameLight'; + +export { default as PushLogoWithNameDark } from './components/PushLogoWithNameDark'; + export { default as Polygon } from './components/Polygon'; export { default as PolygonZK } from './components/PolygonZK'; diff --git a/src/blocks/index.ts b/src/blocks/index.ts index 20c63aa278..678c54372d 100644 --- a/src/blocks/index.ts +++ b/src/blocks/index.ts @@ -1,4 +1,4 @@ -export { Alert, type AlertProps } from './alert'; +export { Alert, type AlertProps, type AlertVariant } from './alert'; export { Box, type BoxProps } from './box'; export { Button, type ButtonProps } from './button'; export { Dropdown, type DropdownProps } from './dropdown'; diff --git a/src/blocks/tabs/Tabs.styled.ts b/src/blocks/tabs/Tabs.styled.ts index cf9c0649fc..bf5b4ffbfa 100644 --- a/src/blocks/tabs/Tabs.styled.ts +++ b/src/blocks/tabs/Tabs.styled.ts @@ -1,15 +1,21 @@ import { Tabs as ReachTabs, TabList, Tab } from '@reach/tabs'; import { textVariants } from '../text'; -import styled from 'styled-components'; +import styled, { CSSProperties } from 'styled-components'; +import { ResponsiveProp } from 'blocks'; import { deviceMediaQ } from '../theme'; +export type TabListProps = { + /* Used to align tabs */ + alignSelf?: ResponsiveProp; +}; + export const StyledFillTabs = styled(ReachTabs)` display: flex; flex-direction: column; gap: var(--spacing-sm); `; -export const StyledFillTabList = styled(TabList)` +export const StyledFillTabList = styled(TabList)` overflow: auto hidden; display: flex; width: fit-content; @@ -17,9 +23,10 @@ export const StyledFillTabList = styled(TabList)` width: -webkit-fill-available; } padding: var(--spacing-xxxs); - background-color: var(--surface-secondary); + background-color: var(--surface-tertiary); border-radius: var(--radius-sm); gap: var(--spacing-xxs); + align-self: ${(props) => props.alignSelf ?? 'flex-start'}; `; export const StyledFillTab = styled(Tab)` @@ -72,13 +79,14 @@ export const StyledLineTabs = styled(ReachTabs)` gap: var(--spacing-sm); `; -export const StyledLineTabList = styled(TabList)` +export const StyledLineTabList = styled(TabList)` overflow: auto hidden; display: flex; background-color: var(--surface-transparent); gap: var(--spacing-xs); justify-content: flex-start; border-bottom: var(--border-sm) solid var(--stroke-secondary); + align-self: ${(props) => props.alignSelf ?? 'flex-start'}; `; export const StyledLineTab = styled(Tab)` diff --git a/src/blocks/tabs/Tabs.tsx b/src/blocks/tabs/Tabs.tsx index 357a50d81e..d7ac8c90a3 100644 --- a/src/blocks/tabs/Tabs.tsx +++ b/src/blocks/tabs/Tabs.tsx @@ -10,6 +10,8 @@ import { StyledLineTabs, StyledTabLabel, } from './Tabs.styled'; +import { ResponsiveProp } from 'blocks'; +import { CSSProperties } from 'styled-components'; export type TabItem = { key: string; @@ -24,9 +26,10 @@ export type TabsProps = { onChange?: (activeKey: string) => void; activeKey?: string; variant?: 'line' | 'fill'; + alignTabs?: ResponsiveProp; }; -const Tabs: React.FC = ({ items, onChange, variant = 'line', activeKey }) => { +const Tabs: React.FC = ({ items, onChange, variant = 'line', activeKey, alignTabs }) => { const handleChange = (index: number) => { const activeItem = items[index]; if (activeItem && !activeItem.disabled) { @@ -49,7 +52,10 @@ const Tabs: React.FC = ({ items, onChange, variant = 'line', activeKe role="tabpanel" keyboardActivation={TabsKeyboardActivation.Auto} > - + {items.map((item) => ( ; @@ -138,3 +147,54 @@ export const channelCategoriesMap: Record = { '0x63381E4b8fE26cb1f55cc38e8369990594E017b1': 'Service', '0x80375eAD5561e19668eb1Dd2b6A44Fa14D5eB6BF': 'Service', }; + +export const purchasePlanAlertConfig: { [x: string]: (planName?: string) => PurchasePlanAlertObjType } = { + success: (planName) => ({ + description: `Purchase Successful. Push ${planName} Plan`, + actionText: 'View on Explorer', + variant: 'success', + }), + renewalReminder: (planName) => ({ + description: `Your Push ${planName} plan ends in 7 days`, + actionText: 'Renew Plan', + variant: 'warning', + }), + expired: (planName) => ({ + description: `Your Push ${planName} plan has expired`, + actionText: 'Renew Plan', + variant: 'error', + }), + notificationLimit: () => ({ + description: `Web2 Notifications limit reached. Upgrade for more features.`, + actionText: 'Upgrade Plan', + variant: 'warning', + }), +}; + +export const faqList: FAQItemTypes[] = [ + { + id: 1, + question: 'What is Push?', + answer: , + }, + { + id: 2, + question: 'What is Push trying to solve?', + answer: , + }, + { + id: 3, + question: 'What are the web3 communication products launched by Push?', + answer: , + }, + { + id: 4, + question: 'How can I use Push as an end-user?', + answer: , + }, + { + id: 5, + question: 'Is Push a blockchain? Is Push decentralised?', + answer: , + }, +]; diff --git a/src/common/Common.types.ts b/src/common/Common.types.ts index 00baba42a2..9743568f1e 100644 --- a/src/common/Common.types.ts +++ b/src/common/Common.types.ts @@ -1,3 +1,6 @@ +import { AlertProps, AlertVariant } from 'blocks'; +import { ReactNode } from 'react'; + export type ModalResponse = { isOpen: boolean; onClose: () => void; @@ -7,3 +10,15 @@ export type ModalResponse = { export type UnlockProfileModalTypes = 'portal' | 'container'; export type EnvType = 'prod' | 'dev' | 'staging'; + +export type PurchasePlanAlertObjType = { + description: AlertProps['description']; + actionText: AlertProps['actionText']; + variant: AlertVariant; +}; + +export type FAQItemTypes = { + id: number; + question: string; + answer: ReactNode; +}; diff --git a/src/common/components/FAQAnswers.tsx b/src/common/components/FAQAnswers.tsx new file mode 100644 index 0000000000..6b4dfe9a3a --- /dev/null +++ b/src/common/components/FAQAnswers.tsx @@ -0,0 +1,272 @@ +import { Box, Text, Link } from 'blocks'; +import { FC } from 'react'; +import { css } from 'styled-components'; + +export const FirstFAQAnswer: FC = () => { + return ( + + + Push is the world’s first blockchain-agnostic decentralised communication protocol for Web3. It is an open + network for validating and indexing all sorts of communication (notifications, chats, etc) that can then be + integrated by any crypto frontend (dApps, wallets, etc). + + + Any smart contract, dApp, or backend service can integrate Push to provide a communication layer through + notifications or chats that are tied to the wallet addresses of users. + + + ); +}; + +export const SecondFAQAnswer: FC = () => { + return ( + + + Push is building the communication layer for Web3, using which any dApp, smart contracts or backend can send any + real time communications (such as notifications, chats, video and more) that are tied directly to a user's + wallet address (aka web3 usernames). + + + This addresses a major gap in the web3 infrastructure, and improving the everyday experience for blockchain + users. The notifications (or any other communications) are off-chain, gasless for all scenarios except when a + smart contract sends them (in which case the smart contract pays a slightly higher gas fees for the payload that + is sent on blockchain). + + + While communications are encrypted and secure, they utilize Push open network which means any dApp or crypto + wallet can easily integrate them making the lives of all web3 users a lot easier and more akin to web2 UX where + apps (or protocols) communicate with their users whenever something of importance occurs or is about to occur + for them. + + + ); +}; + +export const ThirdFAQAnswer: FC = () => { + return ( + + + ⚬ Push Notifications: Enables any smart contract, dApp, backend to deliver critical informations as + notifications to web3 users directly to their wallet addresses. + + + ⚬ Push Chat(wallet-to-wallet chat): Enabling 2-way communication for web3 users from their wallet + addresses. + + + ); +}; + +export const FourthFAQAnswer: FC = () => { + return ( + + + Connect to the{' '} + + Push dApp + {' '} + & opt-in to channels to get notifications for protocols that are relevant to you. Channels are protocols that + activate themselves on Push protocol to send notification. + + + + You can receive notifications from any crypto frontends that have already integrated Push. Alternatively, you + can use via{' '} + + Push dApp + + ,{' '} + + browser extension + + , and mobile app ( + + Android + {' '} + &{' '} + + iOS + + ) in case your favorite wallet or dApp doesn't have Push support yet. + + + + Push recently launched a wallet-to-wallet communication product called Push Chat which is in alpha stage. Reach + out to us on{' '} + + Discord + {' '} + to get exclusive Push Chat access. + + + ); +}; + +export const FifthFAQAnswer: FC = () => { + return ( + + + Push operates on network of nodes called Push Nodes which are responsible for the validation, storage, and + delivery of notifications & chats. + + + Major efforts are put into decentralising Push Nodes which is in the final stages now. Any content or payloads + getting delivered are already immutable and can't be changed as they are secured using crypto-graphical proofs. + The other part which ensures that the content can't be censored is in final stages now of testing and public + alpha push nodes are expected to be rolled out soon. + + + ); +}; diff --git a/src/common/components/FAQContainer.tsx b/src/common/components/FAQContainer.tsx new file mode 100644 index 0000000000..1dd0651322 --- /dev/null +++ b/src/common/components/FAQContainer.tsx @@ -0,0 +1,140 @@ +import { FC, useState } from 'react'; +import { Add, Box, Button, Dash, Front, Link, Text, ArrowUpRight } from 'blocks'; +import { css } from 'styled-components'; +import { faqList } from 'common'; + +export type FAQContainerProps = {}; + +const FAQContainer: FC = ({}) => { + const [expandedQid, setExpandedQid] = useState(null); + + // const answerMapper: { [x: number]: ReactNode } = { + // 1: , + // 2: , + // 3: , + // 4: , + // 5: , + // }; + + return ( + + {/* Render FAQ left side container */} + + + Frequently Asked Questions + + + + + + + {/* Render FAQ Content container */} + + {/* Render list of questions with answers */} + + {faqList.map((faqItem, index) => ( + + + + {faqItem?.question} + + + {expandedQid === faqItem?.id ? ( + setExpandedQid(null)} + size={28} + /> + ) : ( + setExpandedQid(faqItem?.id)} + size={28} + /> + )} + + {expandedQid && expandedQid === faqItem?.id && faqItem.answer} + + ))} + + + {/* Render explore more questions view */} + + + + + Explore FAQs + + + + + + + + ); +}; + +export { FAQContainer }; diff --git a/src/common/components/PurchasePlanAlert.tsx b/src/common/components/PurchasePlanAlert.tsx new file mode 100644 index 0000000000..5ac9b3bb64 --- /dev/null +++ b/src/common/components/PurchasePlanAlert.tsx @@ -0,0 +1,25 @@ +import { Alert } from 'blocks'; +import { PurchasePlanAlertObjType, purchasePlanAlertConfig } from 'common'; +import { FC } from 'react'; + +export type PurchasePlanAlertProps = { + variant: 'success' | 'renewalReminder' | 'expired' | 'notificationLimit'; + onClose?: () => void; + purchasedPlan: { planName: string }; + onAction?: () => void; +}; + +const PurchasePlanAlert: FC = ({ variant, onClose, purchasedPlan, onAction }) => { + const alert: PurchasePlanAlertObjType = purchasePlanAlertConfig[variant](purchasedPlan?.planName); + return ( + onClose?.() : undefined} + onAction={() => onAction?.()} + actionText={alert.actionText} + /> + ); +}; + +export { PurchasePlanAlert }; diff --git a/src/common/components/index.ts b/src/common/components/index.ts index 4b5412cac5..428b091e11 100644 --- a/src/common/components/index.ts +++ b/src/common/components/index.ts @@ -1,3 +1,4 @@ +export * from './FAQAnswers'; export * from './ChannelDetailsCard'; export * from './ContentLayout'; export * from './Stepper'; @@ -10,3 +11,5 @@ export * from './CopyButton'; export * from './VerifiedChannelTooltipContent'; export * from './InAppChannelNotifications'; export * from './InAppChatNotifications'; +export * from './PurchasePlanAlert'; +export * from './FAQContainer'; diff --git a/src/config/AppPaths.ts b/src/config/AppPaths.ts index 6053968b7f..fd9fad8511 100644 --- a/src/config/AppPaths.ts +++ b/src/config/AppPaths.ts @@ -32,6 +32,7 @@ const APP_PATHS = { UserSettings: '/user/settings', ChannelSettings: '/channel/settings', ClaimGalxe: 'claim/galxe', + Pricing: '/pricing', }; export default APP_PATHS; diff --git a/src/modules/createChannel/CreateChannel.tsx b/src/modules/createChannel/CreateChannel.tsx index a23590fa94..3e7d536228 100644 --- a/src/modules/createChannel/CreateChannel.tsx +++ b/src/modules/createChannel/CreateChannel.tsx @@ -5,7 +5,7 @@ import { useNavigate } from 'react-router-dom'; import { Alert, Box } from 'blocks'; import { appConfig } from 'config'; import APP_PATHS from 'config/AppPaths'; -import { Stepper } from 'common'; +import { PurchasePlanAlert, Stepper } from 'common'; import { useAccount } from 'hooks'; import { CHANNEL_TYPE } from 'helpers/UtilityHelper'; import { IPFSupload } from 'helpers/IpfsHelper'; @@ -213,17 +213,25 @@ const CreateChannel = () => { return ( handleCreateNewChannel(values)}> + {/* Use this wrapper to display the Alert of purchased plan status */} + {/* + + */} + diff --git a/src/modules/pricing/Pricing.constants.ts b/src/modules/pricing/Pricing.constants.ts new file mode 100644 index 0000000000..4ca63acf48 --- /dev/null +++ b/src/modules/pricing/Pricing.constants.ts @@ -0,0 +1,136 @@ +import { PricingPlan } from './Pricing.types'; + +export const pricingPlanList: PricingPlan[] = [ + { + id: 1, + planName: 'Basic', + currency: null, + price: 0, + planFor: 'For Casual degens', + isPopular: false, + billingCriteria: '', + planBenefits: [ + { + benefitName: 'Web3 notification', + limit: 'Unlimited', + }, + { + benefitName: 'Telegram Delivery', + limit: 1000, + }, + { + benefitName: 'Email Delivery', + limit: 1000, + }, + { + benefitName: 'No-code logic builder', + limit: null, + }, + ], + }, + { + id: 2, + planName: 'Pro', + currency: '$', + price: 12.49, + planFor: 'For individuals', + isPopular: true, + billingCriteria: 'per month billed yearly', + planBenefits: [ + { + benefitName: 'Web3 notification', + limit: 'Unlimited', + }, + { + benefitName: 'Telegram Delivery', + limit: 1000, + }, + { + benefitName: 'Email Delivery', + limit: 1000, + }, + { + benefitName: 'No-code logic builder', + limit: null, + }, + ], + }, + { + id: 3, + planName: 'Growth', + currency: '$', + price: 42.49, + planFor: 'For growing apps', + isPopular: false, + billingCriteria: 'per month billed yearly', + planBenefits: [ + { + benefitName: 'Web3 notification', + limit: 'Unlimited', + }, + { + benefitName: 'Telegram Delivery', + limit: 50000, + }, + { + benefitName: 'Email Delivery', + limit: 50000, + }, + { + benefitName: 'Discord Delivery', + limit: 50000, + }, + { + benefitName: 'Priority support within 48hrs', + limit: null, + }, + { + benefitName: 'Platform analytics ', + limit: null, + }, + { + benefitName: 'No-code logic builder', + limit: null, + }, + ], + }, + { + id: 4, + planName: 'Enterprise', + currency: null, + price: null, + planFor: 'For advanced solutions', + isPopular: false, + billingCriteria: 'Custom pricing available', + planBenefits: [ + { + benefitName: 'Web3 notification', + limit: 'Unlimited', + }, + { + benefitName: 'Custom Telegram Delivery', + limit: null, + }, + { + benefitName: 'Custom Email Delivery', + limit: null, + }, + { + benefitName: 'Custom Discord Delivery', + limit: null, + }, + { + benefitName: 'Premium support within 24hrs', + limit: null, + }, + { + benefitName: 'Platform analytics ', + limit: null, + }, + { + benefitName: 'No-code logic builder', + limit: null, + }, + ], + }, +]; diff --git a/src/modules/pricing/Pricing.tsx b/src/modules/pricing/Pricing.tsx new file mode 100644 index 0000000000..a7750bc1fa --- /dev/null +++ b/src/modules/pricing/Pricing.tsx @@ -0,0 +1,30 @@ +import { FC } from 'react'; +import { Box } from 'blocks'; +import { FAQContainer } from 'common'; +import { PricingView } from './components/PricingView'; +import { css } from 'styled-components'; + +export type PricingProps = {}; + +const Pricing: FC = () => { + return ( + + {/* Render Pricing View Component */} + + + {/* Render FAQ Component */} + + + ); +}; + +export { Pricing }; diff --git a/src/modules/pricing/Pricing.types.ts b/src/modules/pricing/Pricing.types.ts new file mode 100644 index 0000000000..df79eda571 --- /dev/null +++ b/src/modules/pricing/Pricing.types.ts @@ -0,0 +1,15 @@ +export type PricingPlan = { + id: number; + planName: string; + currency: string | null; + price: number | null; + planFor: string; + isPopular: boolean; + billingCriteria: string; + planBenefits: { + benefitName: string; + limit: string | number | null; + }[]; +}; + +export type PricingPlanTabsType = 'yearly' | 'monthly'; diff --git a/src/modules/pricing/components/PricingPlanTabs.tsx b/src/modules/pricing/components/PricingPlanTabs.tsx new file mode 100644 index 0000000000..b2474a925a --- /dev/null +++ b/src/modules/pricing/components/PricingPlanTabs.tsx @@ -0,0 +1,36 @@ +import { useState } from 'react'; + +import { TabItem, Tabs } from 'blocks'; + +import { PricingPlanTabsType } from '../Pricing.types'; +import { PricingPlansContainer } from './PricingPlansContainer'; + +const PricingPlanTabs = () => { + const pricingPlanTabs: TabItem[] = [ + { + label: 'Yearly', + key: 'yearly', + children: , + }, + { + label: 'Monthly', + key: 'monthly', + children: , + }, + ]; + const [selectedPricingPlanTab, setSelectedPricingPlanTab] = useState( + pricingPlanTabs[0].key as PricingPlanTabsType + ); + + return ( + setSelectedPricingPlanTab(activeKey as PricingPlanTabsType)} + alignTabs="center" + /> + ); +}; + +export { PricingPlanTabs }; diff --git a/src/modules/pricing/components/PricingPlansContainer.tsx b/src/modules/pricing/components/PricingPlansContainer.tsx new file mode 100644 index 0000000000..ebc2291ace --- /dev/null +++ b/src/modules/pricing/components/PricingPlansContainer.tsx @@ -0,0 +1,50 @@ +import { FC } from 'react'; +import { Box, Text } from 'blocks'; +import { PricingPlanTabsType } from '../Pricing.types'; +import { PricingPlansList } from './PricingPlansList'; + +export type PricingPlansContainerProps = { + type: PricingPlanTabsType; +}; + +const PricingPlansContainer: FC = ({ type }) => { + return ( + + + + Save + + + 15% + + + on selecting a yearly plan + + + + {/* Render pricing plans list */} + + + ); +}; + +export { PricingPlansContainer }; diff --git a/src/modules/pricing/components/PricingPlansList.tsx b/src/modules/pricing/components/PricingPlansList.tsx new file mode 100644 index 0000000000..f83b9f44c5 --- /dev/null +++ b/src/modules/pricing/components/PricingPlansList.tsx @@ -0,0 +1,154 @@ +import { FC } from 'react'; +import { Box, Button, Link, Meteor, Tag, Text, Tick } from 'blocks'; +import { pricingPlanList } from '../Pricing.constants'; +import { PricingPlanTabsType } from '../Pricing.types'; +import { css } from 'styled-components'; + +export type PricingPlansListProps = { + type: PricingPlanTabsType; +}; + +const PricingPlansList: FC = () => { + return ( + + {pricingPlanList.map((planItem, planIndex) => ( + + + + + {planItem?.planName} + + {planItem?.isPopular && ( + } + label="Popular" + variant="brand" + size="medium" + /> + )} + + + {planItem?.planFor} + + + + + 0 + ? css` + margin: var(--spacing-none); + ` + : css` + margin: var(--spacing-none) var(--spacing-none) 20px var(--spacing-none); + ` + } + > + + {planItem?.currency}{' '} + {planItem?.price !== null ? (planItem?.price > 0 ? planItem?.price : 'Free') : 'Talk to us!'} + + + {planItem?.billingCriteria} + + + + 0 ? `/pricing/${planIndex}` : '#'}> + + + + + {/* Render the Plan benefit list */} + + {planItem?.planBenefits.map((benefit, benefitIndex) => ( + + + + {benefit?.limit && ( + + {benefit?.limit} + + )} + + {benefit?.benefitName} + + + + ))} + + + ))} + + ); +}; + +export { PricingPlansList }; diff --git a/src/modules/pricing/components/PricingView.tsx b/src/modules/pricing/components/PricingView.tsx new file mode 100644 index 0000000000..0c61b04516 --- /dev/null +++ b/src/modules/pricing/components/PricingView.tsx @@ -0,0 +1,73 @@ +import { FC } from 'react'; +import { css } from 'styled-components'; +import { Box, Link, Text } from 'blocks'; +import { PricingPlanTabs } from './PricingPlanTabs'; + +export type PricingViewProps = {}; + +const PricingView: FC = () => { + return ( + + + + Built to scale with your app. + + + Unlock the power of web3 notifications. + + + Choose a plan that fits your needs. + + + + {/* Render plans tab and list */} + + + + + Have more questions? Get in touch with our + + + sales team. + + + + ); +}; + +export { PricingView }; diff --git a/src/modules/pricing/index.ts b/src/modules/pricing/index.ts new file mode 100644 index 0000000000..5904dfe954 --- /dev/null +++ b/src/modules/pricing/index.ts @@ -0,0 +1 @@ +export { Pricing, type PricingProps } from './Pricing'; diff --git a/src/modules/purchasePlan/PurchasePlan.tsx b/src/modules/purchasePlan/PurchasePlan.tsx new file mode 100644 index 0000000000..17521c1479 --- /dev/null +++ b/src/modules/purchasePlan/PurchasePlan.tsx @@ -0,0 +1,33 @@ +import { FC } from 'react'; +import { Box } from 'blocks'; +import { SelectedPlanView } from './components/SelectedPlanView'; +import { PurchaseSummery } from './components/PurchaseSummery'; +import { pricingPlanList } from 'modules/pricing/Pricing.constants'; +import { toNumber } from 'lodash'; +import { css } from 'styled-components'; + +export type PurchasePlanProps = { + index: string; +}; + +const PurchasePlan: FC = ({ index }) => { + const selectedPlan = pricingPlanList[toNumber(index)]; + return ( + + {/* Render selected plan */} + + {/* Render selected plan */} + + + ); +}; + +export { PurchasePlan }; diff --git a/src/modules/purchasePlan/PusrchasePlan.types.tsx b/src/modules/purchasePlan/PusrchasePlan.types.tsx new file mode 100644 index 0000000000..753bbb07b1 --- /dev/null +++ b/src/modules/purchasePlan/PusrchasePlan.types.tsx @@ -0,0 +1 @@ +export type PurchasePlanModalTypes = 'confirmPurchase' | 'planPurchased' | null; diff --git a/src/modules/purchasePlan/components/ConfirmPurchaseModal.tsx b/src/modules/purchasePlan/components/ConfirmPurchaseModal.tsx new file mode 100644 index 0000000000..50254506dc --- /dev/null +++ b/src/modules/purchasePlan/components/ConfirmPurchaseModal.tsx @@ -0,0 +1,69 @@ +import { FC } from 'react'; +import { Box, Modal, Spinner, Text } from 'blocks'; +import { ModalResponse } from 'common'; + +export type ConfirmPurchaseModalProps = { + purchaseAmount: number; + modalControl: ModalResponse; + onClose: () => void; +}; + +const ConfirmPurchaseModal: FC = ({ purchaseAmount, modalControl, onClose }) => { + const { isOpen } = modalControl; + return ( + + + + + + Confirm purchase + + + + + Purchase Push Pro plan for {purchaseAmount} USDC + + + Confirm the transaction in your wallet + + + + + ); +}; + +export { ConfirmPurchaseModal }; diff --git a/src/modules/purchasePlan/components/PlanPurchasedModal.tsx b/src/modules/purchasePlan/components/PlanPurchasedModal.tsx new file mode 100644 index 0000000000..236968304d --- /dev/null +++ b/src/modules/purchasePlan/components/PlanPurchasedModal.tsx @@ -0,0 +1,102 @@ +import { FC } from 'react'; +import { Box, Modal, PushLogo, PushLogoWithNameDark, PushLogoWithNameLight, Text, Tick } from 'blocks'; +import { PricingPlan } from 'modules/pricing/Pricing.types'; +import { useBlocksTheme } from 'blocks/Blocks.hooks'; +import { ModalResponse } from 'common'; + +export type PlanPurchasedModalProps = { + plan: PricingPlan; + modalControl: ModalResponse; + onClose: () => void; +}; + +const PlanPurchasedModal: FC = ({ plan, modalControl, onClose }) => { + const { mode } = useBlocksTheme(); + const { isOpen } = modalControl; + return ( + + + + {mode === 'light' ? : } + + Your {plan?.planName} plan is now active + + + + + You now have access to the following features: + + + {plan?.planBenefits.map((benefit, benefitIndex) => ( + + + + {benefit?.limit && ( + + {benefit?.limit} + + )} + + {benefit?.benefitName} + + + + ))} + + + + + ); +}; + +export { PlanPurchasedModal }; diff --git a/src/modules/purchasePlan/components/PurchaseSummery.tsx b/src/modules/purchasePlan/components/PurchaseSummery.tsx new file mode 100644 index 0000000000..a9776d61b3 --- /dev/null +++ b/src/modules/purchasePlan/components/PurchaseSummery.tsx @@ -0,0 +1,213 @@ +import { FC, useState } from 'react'; +import { Box, Button, ExternalLink, Link, TabItem, Tabs, Text, TextInput, Tick } from 'blocks'; +import { PricingPlan, PricingPlanTabsType } from 'modules/pricing/Pricing.types'; +import { ConfirmPurchaseModal } from './ConfirmPurchaseModal'; +import { PurchasePlanModalTypes } from '../PusrchasePlan.types'; +import { PlanPurchasedModal } from './PlanPurchasedModal'; +import { useDisclosure } from 'common'; + +export type PurchaseSummeryProps = { selectedPlan: PricingPlan }; +const PurchaseSummery: FC = ({ selectedPlan }) => { + const pricingPlanTabs: TabItem[] = [ + { + label: 'Yearly', + key: 'yearly', + children: null, + }, + { + label: 'Monthly', + key: 'monthly', + children: null, + }, + ]; + + const [selectedPricingPlanTab, setSelectedPricingPlanTab] = useState( + pricingPlanTabs[0].key as PricingPlanTabsType + ); + const [email, setEmail] = useState(''); + const [isApproved, setIsApproved] = useState(false); + const [modalType, setShowModalType] = useState(null); + const modalControl = useDisclosure(); + + const totalAmount = selectedPlan?.price ? selectedPlan?.price * (selectedPricingPlanTab === 'yearly' ? 12 : 1) : 0; + + const handleOnCloseModal = () => { + if (modalType === 'confirmPurchase') { + setShowModalType('planPurchased'); + } else { + modalControl.onClose(); + setShowModalType(null); + } + }; + + return ( + + {/* Render Plan Purchase confirmation modal */} + {modalType === 'confirmPurchase' && ( + + )} + {modalType === 'planPurchased' && ( + + )} + + + Summary + + Each wallet can purchase a limited allocation of Nodes. + + + + {/* Render Summary View */} + + + + Plan + + Push {selectedPlan?.planName} + + + + + Duration + setSelectedPricingPlanTab(activeKey as PricingPlanTabsType)} + alignTabs="center" + /> + + + + setEmail(e.target.value)} + label="Email Address" + /> + + {/* Render total pricing view */} + + + Total Price + {totalAmount} USDC + + Balance: 0 + + + + + + + Get more USDC + + + + + + + {/* Render bottom buttons */} + + + + + + + + + + + ); +}; + +export { PurchaseSummery }; diff --git a/src/modules/purchasePlan/components/SelectedPlanView.tsx b/src/modules/purchasePlan/components/SelectedPlanView.tsx new file mode 100644 index 0000000000..761ed6bb6a --- /dev/null +++ b/src/modules/purchasePlan/components/SelectedPlanView.tsx @@ -0,0 +1,87 @@ +import { FC } from 'react'; +import { Box, Link, Text, Tick } from 'blocks'; +import { PricingPlan } from 'modules/pricing/Pricing.types'; +import { css } from 'styled-components'; + +export type SelectedPlanViewProps = { selectedPlan: PricingPlan }; +const SelectedPlanView: FC = ({ selectedPlan }) => { + return ( + + Push {selectedPlan?.planName} + + change plan + + + + + {selectedPlan?.currency} {selectedPlan?.price} + + + {selectedPlan?.billingCriteria} + + + + {/* Render the Plan benefit list */} + + {selectedPlan?.planBenefits.map((benefit, benefitIndex) => ( + + + + {benefit?.limit && ( + + {benefit?.limit} + + )} + + {benefit?.benefitName} + + + + ))} + + + ); +}; + +export { SelectedPlanView }; diff --git a/src/modules/purchasePlan/index.ts b/src/modules/purchasePlan/index.ts new file mode 100644 index 0000000000..2b84ba1c0b --- /dev/null +++ b/src/modules/purchasePlan/index.ts @@ -0,0 +1 @@ +export { PurchasePlan, type PurchasePlanProps } from './PurchasePlan'; diff --git a/src/pages/PricingPage.tsx b/src/pages/PricingPage.tsx new file mode 100644 index 0000000000..ea00c2072b --- /dev/null +++ b/src/pages/PricingPage.tsx @@ -0,0 +1,13 @@ +import { Pricing } from 'modules/pricing'; +import { ContentLayout } from 'common'; + +// Other Information section +const PricingPage = () => { + // RENDER + return ( + + + + ); +}; +export default PricingPage; diff --git a/src/pages/PurchasePlanPage.tsx b/src/pages/PurchasePlanPage.tsx new file mode 100644 index 0000000000..d7f6390b6b --- /dev/null +++ b/src/pages/PurchasePlanPage.tsx @@ -0,0 +1,16 @@ +import { PurchasePlan } from 'modules/purchasePlan'; +import { ContentLayout } from 'common'; +import { useParams } from 'react-router-dom'; + +// Other Information section +const PurchasePlanPage = () => { + // update spaceid in global space context + let { index } = useParams(); + // RENDER + return ( + + + + ); +}; +export default PurchasePlanPage; diff --git a/src/structure/MasterInterfacePage.tsx b/src/structure/MasterInterfacePage.tsx index e6aba506f1..92330d45ea 100644 --- a/src/structure/MasterInterfacePage.tsx +++ b/src/structure/MasterInterfacePage.tsx @@ -50,6 +50,8 @@ const AddNewChainPage = lazy(() => import('pages/AddNewChain')); const DiscordVerificationPage = lazy(() => import('pages/DiscordVerificationPage')); const SendNotificationPage = lazy(() => import('pages/SendNotificationPage')); +const PricingPage = lazy(() => import('pages/PricingPage')); +const PurchasePlanPage = lazy(() => import('pages/PurchasePlanPage')); // import AirdropPage from 'pages/AirdropPage'; // import ChannelDashboardPage from 'pages/ChannelDashboardPage'; // import ChannelsPage from 'pages/ChannelsPage'; @@ -292,6 +294,14 @@ function MasterInterfacePage() { path="*" element={} /> + } + /> + } + />