diff --git a/packages/webapp/src/components/PricingPlan/PricingPlan.module.scss b/packages/webapp/src/components/PricingPlan/PricingPlan.module.scss index f26a3f2a5..24181bba1 100644 --- a/packages/webapp/src/components/PricingPlan/PricingPlan.module.scss +++ b/packages/webapp/src/components/PricingPlan/PricingPlan.module.scss @@ -23,9 +23,10 @@ color: #fff; text-align: center; font-size: 12px; + text-transform: uppercase; } .label { - font-size: 14px; + font-size: 16px; font-weight: 600; color: #2F343C; @@ -47,13 +48,31 @@ } .price { font-size: 18px; - line-height: 1; - font-weight: 500; - color: #404854; + line-height: 1; + font-weight: 500; + color: #252A31; } .pricePer{ color: #738091; font-size: 12px; line-height: 1; +} + +.featureItem{ + flex: 1; + color: #1C2127; +} + +.featurePopover :global .bp4-popover-content{ + border-radius: 0; +} +.featurePopoverContent{ + font-size: 12px +} +.featurePopoverLabel { + text-transform: uppercase; + letter-spacing: 0.4px; + font-size: 12px; + font-weight: 500; } \ No newline at end of file diff --git a/packages/webapp/src/components/PricingPlan/PricingPlan.tsx b/packages/webapp/src/components/PricingPlan/PricingPlan.tsx index 52bbfecfa..52d23e1a5 100644 --- a/packages/webapp/src/components/PricingPlan/PricingPlan.tsx +++ b/packages/webapp/src/components/PricingPlan/PricingPlan.tsx @@ -1,4 +1,11 @@ -import { Button, ButtonProps, Intent } from '@blueprintjs/core'; +import { + Button, + ButtonProps, + Intent, + Position, + Text, + Tooltip, +} from '@blueprintjs/core'; import clsx from 'classnames'; import { Box, Group, Stack } from '../Layout'; import styles from './PricingPlan.module.scss'; @@ -64,7 +71,7 @@ export interface PricingPriceProps { */ PricingPlan.Price = ({ price, subPrice }: PricingPriceProps) => { return ( - +

{price}

{subPrice}
@@ -101,7 +108,7 @@ export interface PricingFeaturesProps { */ PricingPlan.Features = ({ children }: PricingFeaturesProps) => { return ( - + {children} ); @@ -109,15 +116,41 @@ PricingPlan.Features = ({ children }: PricingFeaturesProps) => { export interface PricingFeatureLineProps { children: React.ReactNode; + hintContent?: string; + hintLabel?: string; } /** * Displays a single feature line within a list of features. * @param children - The content of the feature line. */ -PricingPlan.FeatureLine = ({ children }: PricingFeatureLineProps) => { - return ( - +PricingPlan.FeatureLine = ({ + children, + hintContent, + hintLabel, +}: PricingFeatureLineProps) => { + return hintContent ? ( + + {hintLabel && ( + {hintLabel} + )} + {hintContent} + + } + position={Position.TOP_LEFT} + popoverClassName={styles.featurePopover} + modifiers={{ offset: { enabled: true, offset: '0,10' } }} + minimal + > + + + {children} + + + ) : ( + {children} diff --git a/packages/webapp/src/constants/subscriptionModels.tsx b/packages/webapp/src/constants/subscriptionModels.tsx index 11228be99..37b2d0ef9 100644 --- a/packages/webapp/src/constants/subscriptionModels.tsx +++ b/packages/webapp/src/constants/subscriptionModels.tsx @@ -1,10 +1,92 @@ -// @ts-nocheck -// Subscription plans. -export const plans = [ - -]; +interface SubscriptionPlanFeature { + text: string; + hint?: string; + label?: string; + style?: Record; +} +interface SubscriptionPlan { + name: string; + slug: string; + description: string; + features: SubscriptionPlanFeature[]; + featured?: boolean; + monthlyPrice: string; + monthlyPriceLabel: string; + annuallyPrice: string; + annuallyPriceLabel: string; +} -// Payment methods. -export const paymentMethods = [ - -]; +export const SubscriptionPlans = [ + { + name: 'Capital Basic', + slug: 'capital_basic', + description: 'Good for service businesses that just started.', + features: [ + { + text: 'Unlimited Sale Invoices', + hintLabel: 'Unlimited Sale Invoices', + hint: 'Good for service businesses that just started for service businesses that just started', + }, + { text: 'Unlimated Sale Estimates' }, + { text: 'Track GST and VAT' }, + { text: 'Connect Banks for Automatic Importing' }, + { text: 'Chart of Accounts' }, + { text: 'Manual Journals' }, + { text: 'Basic Financial Reports & Insights' }, + { text: 'Unlimited User Seats' }, + ], + monthlyPrice: '$10', + monthlyPriceLabel: 'Per month', + annuallyPrice: '$7.5', + annuallyPriceLabel: 'Per month', + }, + { + name: 'Capital Essential', + slug: 'capital_plus', + description: 'Good for have inventory and want more financial reports.', + features: [ + { text: 'All Capital Basic features' }, + { text: 'Purchase Invoices' }, + { text: 'Multi Currency Transactions' }, + { text: 'Transactions Locking' }, + { text: 'Inventory Tracking' }, + { text: 'Smart Financial Reports' }, + { text: 'Advanced Inventory Reports' }, + ], + monthlyPrice: '$20', + monthlyPriceLabel: 'Per month', + annuallyPrice: '$15', + annuallyPriceLabel: 'Per month', + }, + { + name: 'Capital Plus', + slug: 'essentials', + description: 'Good for business want financial and access control.', + features: [ + { text: 'All Capital Essential features' }, + { text: 'Custom User Roles Access' }, + { text: 'Vendor Credits' }, + { text: 'Budgeting' }, + { text: 'Analysis Tracking Tags' }, + ], + monthlyPrice: '$25', + monthlyPriceLabel: 'Per month', + annuallyPrice: '$18', + annuallyPriceLabel: 'Per month', + featured: true, + }, + { + name: 'Capital Big', + slug: 'essentials', + description: 'Good for businesses have multiple branches.', + features: [ + { text: 'All Capital Plus features' }, + { text: 'Multiple Branches' }, + { text: 'Multiple Warehouses' }, + ], + monthlyPrice: '$40', + monthlyPriceLabel: 'Per month', + annuallyPrice: '$30', + annuallyPriceLabel: 'Per month', + }, +] as SubscriptionPlan[]; diff --git a/packages/webapp/src/containers/Setup/SetupSubscription/SetupSubscription.module.scss b/packages/webapp/src/containers/Setup/SetupSubscription/SetupSubscription.module.scss index 9fd5de410..7a3087aa0 100644 --- a/packages/webapp/src/containers/Setup/SetupSubscription/SetupSubscription.module.scss +++ b/packages/webapp/src/containers/Setup/SetupSubscription/SetupSubscription.module.scss @@ -3,3 +3,7 @@ margin: 0 auto; padding: 0 40px; } + +.periodSwitch { + margin: 0; +} \ No newline at end of file diff --git a/packages/webapp/src/containers/Setup/SetupSubscription/SubscriptionPlan.tsx b/packages/webapp/src/containers/Setup/SetupSubscription/SubscriptionPlan.tsx index 9f5976bf3..598462531 100644 --- a/packages/webapp/src/containers/Setup/SetupSubscription/SubscriptionPlan.tsx +++ b/packages/webapp/src/containers/Setup/SetupSubscription/SubscriptionPlan.tsx @@ -1,27 +1,51 @@ // @ts-nocheck -import { AppToaster, Group, T } from '@/components'; -import { useGetLemonSqueezyCheckout } from '@/hooks/query'; import { Intent } from '@blueprintjs/core'; +import * as R from 'ramda'; +import { AppToaster } from '@/components'; +import { useGetLemonSqueezyCheckout } from '@/hooks/query'; import { PricingPlan } from '@/components/PricingPlan/PricingPlan'; +import { SubscriptionPlansPeriod } from '@/store/plans/plans.reducer'; +import { + WithPlansProps, + withPlans, +} from '@/containers/Subscriptions/withPlans'; + +interface SubscriptionPricingFeature { + text: string; + hint?: string; + hintLabel?: string; + style?: Record; +} interface SubscriptionPricingProps { slug: string; label: string; description: string; - features?: Array; + features?: Array; featured?: boolean; - price: string; - pricePeriod: string; + monthlyPrice: string; + monthlyPriceLabel: string; + annuallyPrice: string; + annuallyPriceLabel: string; } -function SubscriptionPricing({ - featured, +interface SubscriptionPricingCombinedProps + extends SubscriptionPricingProps, + WithPlansProps {} + +function SubscriptionPlanRoot({ label, description, + featured, features, - price, - pricePeriod, -}: SubscriptionPricingProps) { + monthlyPrice, + monthlyPriceLabel, + annuallyPrice, + annuallyPriceLabel, + + // #withPlans + plansPeriod, +}: SubscriptionPricingCombinedProps) { const { mutateAsync: getLemonCheckout, isLoading } = useGetLemonSqueezyCheckout(); @@ -42,37 +66,34 @@ function SubscriptionPricing({ return ( {featured && Most Popular} - - + + {plansPeriod === SubscriptionPlansPeriod.Monthly ? ( + + ) : ( + + )} Subscribe {features?.map((feature) => ( - {feature} + + {feature.text} + ))} ); } -export function SubscriptionPlans({ plans }) { - return ( - - {plans.map((plan, index) => ( - - ))} - - ); -} +export const SubscriptionPlan = R.compose( + withPlans(({ plansPeriod }) => ({ plansPeriod })), +)(SubscriptionPlanRoot); diff --git a/packages/webapp/src/containers/Setup/SetupSubscription/SubscriptionPlans.tsx b/packages/webapp/src/containers/Setup/SetupSubscription/SubscriptionPlans.tsx new file mode 100644 index 000000000..aab0bb8ce --- /dev/null +++ b/packages/webapp/src/containers/Setup/SetupSubscription/SubscriptionPlans.tsx @@ -0,0 +1,26 @@ +import { Group } from '@/components'; +import { SubscriptionPlan } from './SubscriptionPlan'; +import { useSubscriptionPlans } from './hooks'; + +export function SubscriptionPlans() { + const subscriptionPlans = useSubscriptionPlans(); + + return ( + + {subscriptionPlans.map((plan, index) => ( + + ))} + + ); +} diff --git a/packages/webapp/src/containers/Setup/SetupSubscription/SubscriptionPlansPeriodSwitcher.tsx b/packages/webapp/src/containers/Setup/SetupSubscription/SubscriptionPlansPeriodSwitcher.tsx new file mode 100644 index 000000000..fb7cc23a7 --- /dev/null +++ b/packages/webapp/src/containers/Setup/SetupSubscription/SubscriptionPlansPeriodSwitcher.tsx @@ -0,0 +1,46 @@ +import { ChangeEvent } from 'react'; +import * as R from 'ramda'; +import { Intent, Switch, Tag, Text } from '@blueprintjs/core'; +import { Group } from '@/components'; +import withSubscriptionPlansActions, { + WithSubscriptionPlansActionsProps, +} from '@/containers/Subscriptions/withSubscriptionPlansActions'; +import { SubscriptionPlansPeriod } from '@/store/plans/plans.reducer'; +import styles from './SetupSubscription.module.scss'; + +interface SubscriptionPlansPeriodsSwitchCombinedProps + extends WithSubscriptionPlansActionsProps {} + +function SubscriptionPlansPeriodSwitcherRoot({ + // #withSubscriptionPlansActions + changeSubscriptionPlansPeriod, +}: SubscriptionPlansPeriodsSwitchCombinedProps) { + // Handles the period switch change. + const handleSwitchChange = (event: ChangeEvent) => { + changeSubscriptionPlansPeriod( + event.currentTarget.checked + ? SubscriptionPlansPeriod.Annually + : SubscriptionPlansPeriod.Monthly, + ); + }; + return ( + + Pay Monthly + + + Pay Yearly{' '} + + 25% Off All Year + + + + ); +} + +export const SubscriptionPlansPeriodSwitcher = R.compose( + withSubscriptionPlansActions, +)(SubscriptionPlansPeriodSwitcherRoot); diff --git a/packages/webapp/src/containers/Setup/SetupSubscription/SubscriptionPlansSection.tsx b/packages/webapp/src/containers/Setup/SetupSubscription/SubscriptionPlansSection.tsx index 6795ebf01..764ff8ecd 100644 --- a/packages/webapp/src/containers/Setup/SetupSubscription/SubscriptionPlansSection.tsx +++ b/packages/webapp/src/containers/Setup/SetupSubscription/SubscriptionPlansSection.tsx @@ -1,29 +1,21 @@ -// @ts-nocheck import { Callout } from '@blueprintjs/core'; -import { SubscriptionPlans } from './SubscriptionPlan'; -import withPlans from '../../Subscriptions/withPlans'; -import { compose } from '@/utils'; +import { SubscriptionPlans } from './SubscriptionPlans'; +import { SubscriptionPlansPeriodSwitcher } from './SubscriptionPlansPeriodSwitcher'; /** * Billing plans. */ -function SubscriptionPlansSectionRoot({ plans }) { +export function SubscriptionPlansSection() { return (
- - We're looking for 200 early adopters, when you subscribe you'll get the - full features and unlimited users for a year regardless of the - subscribed plan. + + Simple plans. Simple prices. Only pay for what you really need. All + plans come with award-winning 24/7 customer support. Prices do not + include applicable taxes. - + + +
); } - -export const SubscriptionPlansSection = compose( - withPlans(({ plans }) => ({ plans })), -)(SubscriptionPlansSectionRoot); diff --git a/packages/webapp/src/containers/Setup/SetupSubscription/hooks.ts b/packages/webapp/src/containers/Setup/SetupSubscription/hooks.ts new file mode 100644 index 000000000..3beb2a34b --- /dev/null +++ b/packages/webapp/src/containers/Setup/SetupSubscription/hooks.ts @@ -0,0 +1,5 @@ +import { SubscriptionPlans } from '@/constants/subscriptionModels'; + +export const useSubscriptionPlans = () => { + return SubscriptionPlans; +}; diff --git a/packages/webapp/src/containers/Subscriptions/withPlans.tsx b/packages/webapp/src/containers/Subscriptions/withPlans.tsx index 51c31d93b..5d6c06c6c 100644 --- a/packages/webapp/src/containers/Subscriptions/withPlans.tsx +++ b/packages/webapp/src/containers/Subscriptions/withPlans.tsx @@ -1,17 +1,35 @@ -// @ts-nocheck -import { connect } from 'react-redux'; +import { MapStateToProps, connect } from 'react-redux'; import { + getPlansPeriodSelector, getPlansSelector, } from '@/store/plans/plans.selectors'; +import { ApplicationState } from '@/store/reducers'; -export default (mapState) => { - const mapStateToProps = (state, props) => { +export interface WithPlansProps { + plans: ReturnType>; + plansPeriod: ReturnType>; +} + +type MapState = ( + mapped: WithPlansProps, + state: ApplicationState, + props: Props, +) => any; + +export function withPlans(mapState?: MapState) { + const mapStateToProps: MapStateToProps< + WithPlansProps, + Props, + ApplicationState + > = (state, props) => { const getPlans = getPlansSelector(); + const getPlansPeriod = getPlansPeriodSelector(); const mapped = { - plans: getPlans(state, props), + plans: getPlans(state), + plansPeriod: getPlansPeriod(state), }; return mapState ? mapState(mapped, state, props) : mapped; }; return connect(mapStateToProps); -}; +} diff --git a/packages/webapp/src/containers/Subscriptions/withSubscriptionPlansActions.tsx b/packages/webapp/src/containers/Subscriptions/withSubscriptionPlansActions.tsx index 330af343a..67f6868f9 100644 --- a/packages/webapp/src/containers/Subscriptions/withSubscriptionPlansActions.tsx +++ b/packages/webapp/src/containers/Subscriptions/withSubscriptionPlansActions.tsx @@ -1,9 +1,22 @@ -// @ts-nocheck -import { connect } from 'react-redux'; -import { initSubscriptionPlans } from '@/store/plans/plans.actions'; +import { MapDispatchToProps, connect } from 'react-redux'; +import { + SubscriptionPlansPeriod, + changePlansPeriod, + initSubscriptionPlans, +} from '@/store/plans/plans.reducer'; -export const mapDispatchToProps = (dispatch) => ({ +export interface WithSubscriptionPlansActionsProps { + initSubscriptionPlans: () => void; + changeSubscriptionPlansPeriod: (period: SubscriptionPlansPeriod) => void; +} + +export const mapDispatchToProps: MapDispatchToProps< + WithSubscriptionPlansActionsProps, + {} +> = (dispatch: any) => ({ initSubscriptionPlans: () => dispatch(initSubscriptionPlans()), + changeSubscriptionPlansPeriod: (period: SubscriptionPlansPeriod) => + dispatch(changePlansPeriod({ period })), }); -export default connect(null, mapDispatchToProps); \ No newline at end of file +export default connect(null, mapDispatchToProps); diff --git a/packages/webapp/src/store/plans/plans.reducer.tsx b/packages/webapp/src/store/plans/plans.reducer.tsx index 1f2878205..647db3b48 100644 --- a/packages/webapp/src/store/plans/plans.reducer.tsx +++ b/packages/webapp/src/store/plans/plans.reducer.tsx @@ -1,70 +1,46 @@ -// @ts-nocheck -import { createReducer } from '@reduxjs/toolkit'; -import t from '@/store/types'; +import { PayloadAction, createSlice } from '@reduxjs/toolkit'; +import { SubscriptionPlans } from '@/constants/subscriptionModels'; -const getSubscriptionPlans = () => [ - { - name: 'Capital Basic', - slug: 'capital_basic', - description: 'Good for service businesses that just started.', - features: [ - 'Sale Invoices and Estimates', - 'Tracking Expenses', - 'Customize Invoice', - 'Manual Journals', - 'Bank Reconciliation', - 'Chart of Accounts', - 'Taxes', - 'Basic Financial Reports & Insights', - ], - price: '$29', - pricePeriod: 'Per Year', - }, - { - name: 'Capital Plus', - slug: 'capital_plus', - description: - 'Good for businesses have inventory and want more financial reports.', - features: [ - 'All Capital Basic features', - 'Manage Bills', - 'Inventory Tracking', - 'Multi Currencies', - 'Predefined user roles.', - 'Transactions locking.', - 'Smart Financial Reports.', - ], - price: '$29', - pricePeriod: 'Per Year', - featured: true, - }, - { - name: 'Capital Big', - slug: 'essentials', - description: 'Good for businesses have multiple inventory or branches.', - features: [ - 'All Capital Plus features', - 'Multiple Warehouses', - 'Multiple Branches', - 'Invite >= 15 Users', - ], - price: '$29', - pricePeriod: 'Per Year', - }, -]; +export enum SubscriptionPlansPeriod { + Monthly = 'monthly', + Annually = 'Annually', +} -const initialState = { - plans: [], - periods: [], -}; +interface StorePlansState { + plans: any; + plansPeriod: SubscriptionPlansPeriod; +} -export default createReducer(initialState, { - /** - * Initialize the subscription plans. - */ - [t.INIT_SUBSCRIPTION_PLANS]: (state) => { - const plans = getSubscriptionPlans(); +export const SubscriptionPlansSlice = createSlice({ + name: 'plans', + initialState: { + plans: [], + periods: [], + plansPeriod: 'monthly', + } as StorePlansState, + reducers: { + /** + * Initialize the subscription plans. + * @param {StorePlansState} state + */ + initSubscriptionPlans: (state: StorePlansState) => { + const plans = SubscriptionPlans; + state.plans = plans; + }, - state.plans = plans; + /** + * Changes the plans period (monthly or annually). + * @param {StorePlansState} state + * @param {PayloadAction<{ period: SubscriptionPlansPeriod }>} action + */ + changePlansPeriod: ( + state: StorePlansState, + action: PayloadAction<{ period: SubscriptionPlansPeriod }>, + ) => { + state.plansPeriod = action.payload.period; + }, }, }); + +export const { initSubscriptionPlans, changePlansPeriod } = + SubscriptionPlansSlice.actions; diff --git a/packages/webapp/src/store/plans/plans.selectors.tsx b/packages/webapp/src/store/plans/plans.selectors.tsx index cd75bfbb8..d10760272 100644 --- a/packages/webapp/src/store/plans/plans.selectors.tsx +++ b/packages/webapp/src/store/plans/plans.selectors.tsx @@ -2,19 +2,21 @@ import { createSelector } from 'reselect'; const plansSelector = (state) => state.plans.plans; -const planSelector = (state, props) => state.plans.plans - .find((plan) => plan.slug === props.planSlug); +const planSelector = (state, props) => + state.plans.plans.find((plan) => plan.slug === props.planSlug); + +const plansPeriodSelector = (state) => state.plans.plansPeriod; // Retrieve manual jounral current page results. -export const getPlansSelector = () => createSelector( - plansSelector, - (plans) => { +export const getPlansSelector = () => + createSelector(plansSelector, (plans) => { return plans; - }, -); + }); // Retrieve plan details. -export const getPlanSelector = () => createSelector( - planSelector, - (plan) => plan, -) \ No newline at end of file +export const getPlanSelector = () => + createSelector(planSelector, (plan) => plan); + +// Retrieves the plans period (monthly or annually). +export const getPlansPeriodSelector = () => + createSelector(plansPeriodSelector, (periods) => periods); diff --git a/packages/webapp/src/store/reducers.tsx b/packages/webapp/src/store/reducers.tsx index ddcc6ff27..aa70e7cd3 100644 --- a/packages/webapp/src/store/reducers.tsx +++ b/packages/webapp/src/store/reducers.tsx @@ -32,13 +32,17 @@ import paymentMades from './PaymentMades/paymentMades.reducer'; import organizations from './organizations/organizations.reducers'; import subscriptions from './subscription/subscription.reducer'; import inventoryAdjustments from './inventoryAdjustments/inventoryAdjustment.reducer'; -import plans from './plans/plans.reducer'; +import { SubscriptionPlansSlice } from './plans/plans.reducer'; import creditNotes from './CreditNote/creditNote.reducer'; import vendorCredit from './VendorCredit/VendorCredit.reducer'; import warehouseTransfers from './WarehouseTransfer/warehouseTransfer.reducer'; import projects from './Project/projects.reducer'; import { PlaidSlice } from './banking/banking.reducer'; +export interface ApplicationState { + +} + const appReducer = combineReducers({ authentication, organizations, @@ -69,7 +73,7 @@ const appReducer = combineReducers({ paymentReceives, paymentMades, inventoryAdjustments, - plans, + plans: SubscriptionPlansSlice.reducer, creditNotes, vendorCredit, warehouseTransfers,