From eb3f23554fbaf30b472d8096bffc81698a9d73a1 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sat, 13 Jul 2024 18:19:18 +0200 Subject: [PATCH 1/3] feat: upgrade the subscription plans --- .../PricingPlan/PricingPlan.module.scss | 27 ++++- .../components/PricingPlan/PricingPlan.tsx | 45 +++++++- .../src/constants/subscriptionModels.tsx | 100 +++++++++++++++-- .../SetupSubscription.module.scss | 4 + .../SetupSubscription/SubscriptionPlan.tsx | 83 ++++++++------ .../SetupSubscription/SubscriptionPlans.tsx | 26 +++++ .../SubscriptionPlansPeriodSwitcher.tsx | 46 ++++++++ .../SubscriptionPlansSection.tsx | 28 ++--- .../Setup/SetupSubscription/hooks.ts | 5 + .../containers/Subscriptions/withPlans.tsx | 30 ++++- .../withSubscriptionPlansActions.tsx | 23 +++- .../webapp/src/store/plans/plans.reducer.tsx | 104 +++++++----------- .../src/store/plans/plans.selectors.tsx | 24 ++-- packages/webapp/src/store/reducers.tsx | 8 +- 14 files changed, 397 insertions(+), 156 deletions(-) create mode 100644 packages/webapp/src/containers/Setup/SetupSubscription/SubscriptionPlans.tsx create mode 100644 packages/webapp/src/containers/Setup/SetupSubscription/SubscriptionPlansPeriodSwitcher.tsx create mode 100644 packages/webapp/src/containers/Setup/SetupSubscription/hooks.ts diff --git a/packages/webapp/src/components/PricingPlan/PricingPlan.module.scss b/packages/webapp/src/components/PricingPlan/PricingPlan.module.scss index f26a3f2a5e..24181bba1c 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 52bbfecfa3..52d23e1a57 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 11228be998..37b2d0ef9f 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 9fd5de4109..7a3087aa0f 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 9f5976bf38..5984625313 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 0000000000..aab0bb8ce1 --- /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 0000000000..fb7cc23a75 --- /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 6795ebf014..764ff8ecde 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 0000000000..3beb2a34be --- /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 51c31d93b3..5d6c06c6ca 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 330af343ae..67f6868f9e 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 1f28782055..647db3b485 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 cd75bfbb8e..d107602726 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 ddcc6ff27c..aa70e7cd3e 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, From 72128a72c4c7737d6854033cee9d1cb0f547fbac Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sat, 13 Jul 2024 19:53:52 +0200 Subject: [PATCH 2/3] feat: add variant ids to new subscription plans --- packages/webapp/src/constants/subscriptionModels.tsx | 10 ++++++++++ .../Setup/SetupSubscription/SubscriptionPlan.tsx | 11 ++++++++++- .../Setup/SetupSubscription/SubscriptionPlans.tsx | 2 ++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/webapp/src/constants/subscriptionModels.tsx b/packages/webapp/src/constants/subscriptionModels.tsx index 37b2d0ef9f..ab16d07edd 100644 --- a/packages/webapp/src/constants/subscriptionModels.tsx +++ b/packages/webapp/src/constants/subscriptionModels.tsx @@ -14,6 +14,8 @@ interface SubscriptionPlan { monthlyPriceLabel: string; annuallyPrice: string; annuallyPriceLabel: string; + monthlyVariantId: string; + annuallyVariantId: string; } export const SubscriptionPlans = [ @@ -39,6 +41,8 @@ export const SubscriptionPlans = [ monthlyPriceLabel: 'Per month', annuallyPrice: '$7.5', annuallyPriceLabel: 'Per month', + monthlyVariantId: '446152', + annuallyVariantId: '446153', }, { name: 'Capital Essential', @@ -57,6 +61,8 @@ export const SubscriptionPlans = [ monthlyPriceLabel: 'Per month', annuallyPrice: '$15', annuallyPriceLabel: 'Per month', + monthlyVariantId: '446165', + annuallyVariantId: '446164', }, { name: 'Capital Plus', @@ -74,6 +80,8 @@ export const SubscriptionPlans = [ annuallyPrice: '$18', annuallyPriceLabel: 'Per month', featured: true, + monthlyVariantId: '446165', + annuallyVariantId: '446164', }, { name: 'Capital Big', @@ -88,5 +96,7 @@ export const SubscriptionPlans = [ monthlyPriceLabel: 'Per month', annuallyPrice: '$30', annuallyPriceLabel: 'Per month', + monthlyVariantId: '446167', + annuallyVariantId: '446168', }, ] as SubscriptionPlan[]; diff --git a/packages/webapp/src/containers/Setup/SetupSubscription/SubscriptionPlan.tsx b/packages/webapp/src/containers/Setup/SetupSubscription/SubscriptionPlan.tsx index 5984625313..4ebb88d5f3 100644 --- a/packages/webapp/src/containers/Setup/SetupSubscription/SubscriptionPlan.tsx +++ b/packages/webapp/src/containers/Setup/SetupSubscription/SubscriptionPlan.tsx @@ -27,6 +27,8 @@ interface SubscriptionPricingProps { monthlyPriceLabel: string; annuallyPrice: string; annuallyPriceLabel: string; + monthlyVariantId?: string; + annuallyVariantId?: string; } interface SubscriptionPricingCombinedProps @@ -42,6 +44,8 @@ function SubscriptionPlanRoot({ monthlyPriceLabel, annuallyPrice, annuallyPriceLabel, + monthlyVariantId, + annuallyVariantId, // #withPlans plansPeriod, @@ -50,7 +54,12 @@ function SubscriptionPlanRoot({ useGetLemonSqueezyCheckout(); const handleClick = () => { - getLemonCheckout({ variantId: '338516' }) + const variantId = + SubscriptionPlansPeriod.Monthly === plansPeriod + ? monthlyVariantId + : annuallyVariantId; + + getLemonCheckout({ variantId }) .then((res) => { const checkoutUrl = res.data.data.attributes.url; window.LemonSqueezy.Url.Open(checkoutUrl); diff --git a/packages/webapp/src/containers/Setup/SetupSubscription/SubscriptionPlans.tsx b/packages/webapp/src/containers/Setup/SetupSubscription/SubscriptionPlans.tsx index aab0bb8ce1..7fa489f0e8 100644 --- a/packages/webapp/src/containers/Setup/SetupSubscription/SubscriptionPlans.tsx +++ b/packages/webapp/src/containers/Setup/SetupSubscription/SubscriptionPlans.tsx @@ -19,6 +19,8 @@ export function SubscriptionPlans() { monthlyPriceLabel={plan.monthlyPriceLabel} annuallyPrice={plan.annuallyPrice} annuallyPriceLabel={plan.annuallyPriceLabel} + monthlyVariantId={plan.monthlyVariantId} + annuallyVariantId={plan.annuallyVariantId} /> ))} From 67d155759e8216fd96b7165ffa3ab156a6136971 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sun, 14 Jul 2024 14:19:04 +0200 Subject: [PATCH 3/3] feat: backend the new monthly susbcription plans --- .../Subscription/LemonSqueezyWebhooks.ts | 25 +---- ..._lemon_variant_id_to_subscription_plans.js | 11 +++ ...4101229_seed_monthly_subscription_plans.js | 96 +++++++++++++++++++ .../src/constants/subscriptionModels.tsx | 62 +++++++++--- 4 files changed, 160 insertions(+), 34 deletions(-) create mode 100644 packages/server/src/system/migrations/20240714101006_add_lemon_variant_id_to_subscription_plans.js create mode 100644 packages/server/src/system/migrations/20240714101229_seed_monthly_subscription_plans.js diff --git a/packages/server/src/services/Subscription/LemonSqueezyWebhooks.ts b/packages/server/src/services/Subscription/LemonSqueezyWebhooks.ts index 6a7c3966d7..0be4dac350 100644 --- a/packages/server/src/services/Subscription/LemonSqueezyWebhooks.ts +++ b/packages/server/src/services/Subscription/LemonSqueezyWebhooks.ts @@ -1,4 +1,3 @@ -import { getPrice } from '@lemonsqueezy/lemonsqueezy.js'; import config from '@/config'; import { Inject, Service } from 'typedi'; import { @@ -10,7 +9,6 @@ import { } from './utils'; import { Plan } from '@/system/models'; import { Subscription } from './Subscription'; -import { isEmpty } from 'lodash'; @Service() export class LemonSqueezyWebhooks { @@ -18,7 +16,7 @@ export class LemonSqueezyWebhooks { private subscriptionService: Subscription; /** - * handle the LemonSqueezy webhooks. + * Handles the Lemon Squeezy webhooks. * @param {string} rawBody * @param {string} signature * @returns {Promise} @@ -74,7 +72,7 @@ export class LemonSqueezyWebhooks { const variantId = attributes.variant_id as string; // We assume that the Plan table is up to date. - const plan = await Plan.query().findOne('slug', 'early-adaptor'); + const plan = await Plan.query().findOne('lemonVariantId', variantId); if (!plan) { throw new Error(`Plan with variantId ${variantId} not found.`); @@ -82,26 +80,9 @@ export class LemonSqueezyWebhooks { // Update the subscription in the database. const priceId = attributes.first_subscription_item.price_id; - // Get the price data from Lemon Squeezy. - const priceData = await getPrice(priceId); - - if (priceData.error) { - throw new Error( - `Failed to get the price data for the subscription ${eventBody.data.id}.` - ); - } - const isUsageBased = - attributes.first_subscription_item.is_usage_based; - const price = isUsageBased - ? priceData.data?.data.attributes.unit_price_decimal - : priceData.data?.data.attributes.unit_price; - // Create a new subscription of the tenant. if (webhookEvent === 'subscription_created') { - await this.subscriptionService.newSubscribtion( - tenantId, - 'early-adaptor' - ); + await this.subscriptionService.newSubscribtion(tenantId, plan.slug); } } } else if (webhookEvent.startsWith('order_')) { diff --git a/packages/server/src/system/migrations/20240714101006_add_lemon_variant_id_to_subscription_plans.js b/packages/server/src/system/migrations/20240714101006_add_lemon_variant_id_to_subscription_plans.js new file mode 100644 index 0000000000..eeee3581f3 --- /dev/null +++ b/packages/server/src/system/migrations/20240714101006_add_lemon_variant_id_to_subscription_plans.js @@ -0,0 +1,11 @@ +exports.up = function (knex) { + return knex.schema.table('subscription_plans', (table) => { + table.string('lemon_variant_id').nullable().index(); + }); +}; + +exports.down = (knex) => { + return knex.schema.table('subscription_plans', (table) => { + table.dropColumn('lemon_variant_id'); + }); +}; diff --git a/packages/server/src/system/migrations/20240714101229_seed_monthly_subscription_plans.js b/packages/server/src/system/migrations/20240714101229_seed_monthly_subscription_plans.js new file mode 100644 index 0000000000..7f1e506ac0 --- /dev/null +++ b/packages/server/src/system/migrations/20240714101229_seed_monthly_subscription_plans.js @@ -0,0 +1,96 @@ +exports.up = function (knex) { + return knex('subscription_plans').insert([ + // Capital Basic + { + name: 'Capital Basic (Monthly)', + slug: 'capital-basic-monthly', + price: 10, + active: true, + currency: 'USD', + invoice_period: 1, + invoice_interval: 'month', + lemon_variant_id: '446152', + // lemon_variant_id: '450016', + }, + { + name: 'Capital Basic (Annually)', + slug: 'capital-basic-annually', + price: 90, + active: true, + currency: 'USD', + invoice_period: 1, + invoice_interval: 'year', + lemon_variant_id: '446153', + // lemon_variant_id: '450018', + }, + + // # Capital Essential + { + name: 'Capital Essential (Monthly)', + slug: 'capital-essential-monthly', + price: 20, + active: true, + currency: 'USD', + invoice_period: 1, + invoice_interval: 'month', + lemon_variant_id: '446155', + // lemon_variant_id: '450028', + }, + { + name: 'Capital Essential (Annually)', + slug: 'capital-essential-annually', + price: 180, + active: true, + invoice_period: 1, + invoice_interval: 'year', + lemon_variant_id: '446156', + // lemon_variant_id: '450029', + }, + + // # Capital Plus + { + name: 'Capital Plus (Monthly)', + slug: 'capital-plus-monthly', + price: 25, + active: true, + invoice_period: 1, + invoice_interval: 'month', + lemon_variant_id: '446165', + // lemon_variant_id: '450031', + }, + { + name: 'Capital Plus (Annually)', + slug: 'capital-plus-annually', + price: 228, + active: true, + invoice_period: 1, + invoice_interval: 'year', + lemon_variant_id: '446164', + // lemon_variant_id: '450032', + }, + + // # Capital Big + { + name: 'Capital Big (Monthly)', + slug: 'capital-big-monthly', + price: 40, + active: true, + invoice_period: 1, + invoice_interval: 'month', + lemon_variant_id: '446167', + // lemon_variant_id: '450024', + }, + { + name: 'Capital Big (Annually)', + slug: 'capital-big-annually', + price: 360, + active: true, + invoice_period: 1, + invoice_interval: 'year', + lemon_variant_id: '446168', + // lemon_variant_id: '450025', + }, + ]); +}; + +exports.down = function (knex) {}; diff --git a/packages/webapp/src/constants/subscriptionModels.tsx b/packages/webapp/src/constants/subscriptionModels.tsx index ab16d07edd..e1a6ba35ce 100644 --- a/packages/webapp/src/constants/subscriptionModels.tsx +++ b/packages/webapp/src/constants/subscriptionModels.tsx @@ -33,8 +33,15 @@ export const SubscriptionPlans = [ { text: 'Track GST and VAT' }, { text: 'Connect Banks for Automatic Importing' }, { text: 'Chart of Accounts' }, - { text: 'Manual Journals' }, - { text: 'Basic Financial Reports & Insights' }, + { + text: 'Manual Journals', + hintLabel: 'Manual Journals', + hint: 'Write manual journals entries for financial transactions not automatically captured by the system to adjust financial statements.', + }, + { + text: 'Basic Financial Reports & Insights', + hint: 'Balance sheet, profit & loss statement, cashflow statement, general ledger, journal sheet, A/P aging summary, A/R aging summary', + }, { text: 'Unlimited User Seats' }, ], monthlyPrice: '$10', @@ -42,7 +49,9 @@ export const SubscriptionPlans = [ annuallyPrice: '$7.5', annuallyPriceLabel: 'Per month', monthlyVariantId: '446152', + // monthlyVariantId: '450016', annuallyVariantId: '446153', + // annuallyVariantId: '450018', }, { name: 'Capital Essential', @@ -51,9 +60,21 @@ export const SubscriptionPlans = [ features: [ { text: 'All Capital Basic features' }, { text: 'Purchase Invoices' }, - { text: 'Multi Currency Transactions' }, - { text: 'Transactions Locking' }, - { text: 'Inventory Tracking' }, + { + text: 'Multi Currency Transactions', + hintLabel: 'Multi Currency', + hint: 'Pay and get paid and do manual journals in any currency with real time exchange rates conversions.', + }, + { + text: 'Transactions Locking', + hintLabel: 'Transactions Locking', + hint: 'Transaction Locking freezes transactions to prevent any additions, modifications, or deletions of transactions recorded during the specified date.', + }, + { + text: 'Inventory Tracking', + hintLabel: 'Inventory Tracking', + hint: 'Track goods in the stock, cost of goods, and get notifications when quantity is low.', + }, { text: 'Smart Financial Reports' }, { text: 'Advanced Inventory Reports' }, ], @@ -61,8 +82,10 @@ export const SubscriptionPlans = [ monthlyPriceLabel: 'Per month', annuallyPrice: '$15', annuallyPriceLabel: 'Per month', - monthlyVariantId: '446165', - annuallyVariantId: '446164', + // monthlyVariantId: '450028', + monthlyVariantId: '446155', + // annuallyVariantId: '450029', + annuallyVariantId: '446156', }, { name: 'Capital Plus', @@ -72,15 +95,20 @@ export const SubscriptionPlans = [ { text: 'All Capital Essential features' }, { text: 'Custom User Roles Access' }, { text: 'Vendor Credits' }, - { text: 'Budgeting' }, - { text: 'Analysis Tracking Tags' }, + { + text: 'Budgeting', + hint: 'Create multiple budgets and compare targets with actuals to understand how your business is performing.', + }, + { text: 'Analysis Cost Center' }, ], monthlyPrice: '$25', monthlyPriceLabel: 'Per month', - annuallyPrice: '$18', + annuallyPrice: '$19', annuallyPriceLabel: 'Per month', featured: true, + // monthlyVariantId: '450031', monthlyVariantId: '446165', + // annuallyVariantId: '450032', annuallyVariantId: '446164', }, { @@ -89,14 +117,24 @@ export const SubscriptionPlans = [ description: 'Good for businesses have multiple branches.', features: [ { text: 'All Capital Plus features' }, - { text: 'Multiple Branches' }, - { text: 'Multiple Warehouses' }, + { + text: 'Multiple Branches', + hintLabel: '', + hint: 'Track the organization transactions and accounts in multiple branches.', + }, + { + text: 'Multiple Warehouses', + hintLabel: 'Multiple Warehouses', + hint: 'Track the organization inventory in multiple warehouses and transfer goods between them.', + }, ], monthlyPrice: '$40', monthlyPriceLabel: 'Per month', annuallyPrice: '$30', annuallyPriceLabel: 'Per month', + // monthlyVariantId: '450024', monthlyVariantId: '446167', + // annuallyVariantId: '450025', annuallyVariantId: '446168', }, ] as SubscriptionPlan[];