From 0c9816ba57b70c71ae80fe8d092ce5efb9cd9456 Mon Sep 17 00:00:00 2001 From: Nick Grato Date: Wed, 20 Dec 2023 11:18:28 -0800 Subject: [PATCH] stache (#432) * stache * more plan logic * adding contact grid plan guarding support --------- Co-authored-by: Nick Grato --- .../modules/hubs/HubFormCard/HubFormCard.tsx | 6 +- .../plans/ContactCard/ContactCard.module.scss | 6 +- .../modules/plans/ContactCard/ContactCard.tsx | 2 +- .../plans/plan-cards/BusinessPlanCard.tsx | 59 ++++++++ .../modules/plans/plan-cards/index.ts | 1 + client/components/modules/plans/plan.const.ts | 35 ++++- .../side-panel/BookMeeting/BookMeeting.tsx | 16 ++- .../side-panel/SidePanel/SidePanel.tsx | 6 +- .../side-panel/SupportGrid/SupportGrid.tsx | 52 ++++++- client/pages/analytics/index.tsx | 14 +- client/pages/subscribe/index.tsx | 32 +++-- client/pages/subscribe/subscribe.module.scss | 10 +- client/services/routeGuard.service.ts | 42 +++++- client/styles/tools/flex.scss | 5 + lib/dash/plan_state_machine.ex | 136 ++++++++++++++++++ .../BasePlanCard/BasePlanCard.module.scss | 1 - .../shared/Subscribe/Subscribe.module.scss | 10 +- .../components/shared/Subscribe/Subscribe.tsx | 50 ++++++- .../components/shared/Subscribe/plan.const.ts | 29 +++- 19 files changed, 449 insertions(+), 63 deletions(-) create mode 100644 client/components/modules/plans/plan-cards/BusinessPlanCard.tsx diff --git a/client/components/modules/hubs/HubFormCard/HubFormCard.tsx b/client/components/modules/hubs/HubFormCard/HubFormCard.tsx index d1b05003..0699da91 100644 --- a/client/components/modules/hubs/HubFormCard/HubFormCard.tsx +++ b/client/components/modules/hubs/HubFormCard/HubFormCard.tsx @@ -12,7 +12,7 @@ import { StoreContext, SubdomainRetryT } from 'contexts/StoreProvider'; import { RoutesE } from 'types/Routes'; import { useFormik } from 'formik'; import validate, { FormValues } from './validate'; -import { useIsProfessional } from 'hooks/usePlans'; +import { useIsProfessionalUp } from 'hooks/usePlans'; import Hub from 'classes/Hub'; import { HubT } from 'types/General'; import SecretCopy from '@Shared/SecretCopy/SecretCopy'; @@ -38,7 +38,7 @@ const HubFormCard = ({ hub: _hub, classProp = '' }: HubFormCardPropsT) => { useState(''); const [isEditingDomain, setIsEditingDomain] = useState(false); const router = useRouter(); - const isProfessional = useIsProfessional(); + const isProfessionalUp = useIsProfessionalUp(); const hub = useMemo(() => new Hub(_hub), [_hub]); /** @@ -281,7 +281,7 @@ const HubFormCard = ({ hub: _hub, classProp = '' }: HubFormCardPropsT) => { ) : null} - {isProfessional && ( + {isProfessionalUp && (

Custom Domain

diff --git a/client/components/modules/plans/ContactCard/ContactCard.module.scss b/client/components/modules/plans/ContactCard/ContactCard.module.scss index f923b0e2..10c030fc 100644 --- a/client/components/modules/plans/ContactCard/ContactCard.module.scss +++ b/client/components/modules/plans/ContactCard/ContactCard.module.scss @@ -2,9 +2,9 @@ .wrapper { background-color: $color-background-overlay; - width: 375px; + width: 100%; border-radius: 20px; - padding: 65px; + padding: 25px; overflow: hidden; position: relative; margin: 0px 17px 24px; @@ -24,7 +24,7 @@ h2 { @include heading-lg; - margin: 0 0 24px; + margin: 0 0 12px; } p { diff --git a/client/components/modules/plans/ContactCard/ContactCard.tsx b/client/components/modules/plans/ContactCard/ContactCard.tsx index a198d812..e51c8387 100644 --- a/client/components/modules/plans/ContactCard/ContactCard.tsx +++ b/client/components/modules/plans/ContactCard/ContactCard.tsx @@ -15,7 +15,7 @@ const ContactCard = ({ email, subject, classProp = '' }: ContactCardPropsT) => { return (
-

Business

+

Enterprise

Need dedicated infrastructure, custom clients, or something else?

diff --git a/client/components/modules/side-panel/SidePanel/SidePanel.tsx b/client/components/modules/side-panel/SidePanel/SidePanel.tsx index 4128e041..8393edf5 100644 --- a/client/components/modules/side-panel/SidePanel/SidePanel.tsx +++ b/client/components/modules/side-panel/SidePanel/SidePanel.tsx @@ -9,7 +9,7 @@ import GettingStartedPanel from '../GettingStartedPanel/GettingStartedPanel'; import { useMobileDown } from 'hooks/useMediaQuery'; import { useSelector } from 'react-redux'; import { selectAccount } from 'store/accountSlice'; -import { useIsProfessional } from 'hooks/usePlans'; +import { useIsBusiness } from 'hooks/usePlans'; export type SidePanelPropsT = { fullDomain: string; @@ -24,7 +24,7 @@ const SidePanel = ({ }: SidePanelPropsT) => { const account = useSelector(selectAccount); const isMobile = useMobileDown(); - const isProfessional = useIsProfessional(); + const isBusiness = useIsBusiness(); return (
@@ -55,7 +55,7 @@ const SidePanel = ({ {account.hasSubscription && subscription.isCancelled && ( )} - {!isProfessional && } + {!isBusiness && } diff --git a/client/components/modules/side-panel/SupportGrid/SupportGrid.tsx b/client/components/modules/side-panel/SupportGrid/SupportGrid.tsx index 0e8fffe8..66897dc9 100644 --- a/client/components/modules/side-panel/SupportGrid/SupportGrid.tsx +++ b/client/components/modules/side-panel/SupportGrid/SupportGrid.tsx @@ -1,19 +1,61 @@ +import { useState, useEffect, useMemo } from 'react'; import ExpansionPanel from '@Shared/ExpansionPanel/ExpansionPanel'; import SupportLink from '@Shared/SupportLink/SupportLink'; import styles from './SupportGrid.module.scss'; -import { useIsStarter } from 'hooks/usePlans'; +import { useIsProfessionalUp } from 'hooks/usePlans'; +import { PlansE } from 'types/General'; import mailCircle from 'public/mail_circle.png'; import messageCircle from 'public/message_circle.png'; import questionCircle from 'public/question_circle.png'; import BookMeeting from '../BookMeeting/BookMeeting'; +import { selectAccount } from 'store/accountSlice'; +import { useSelector } from 'react-redux'; + +type PlanContactInfoT = { + email: string; + calendar: string; +}; + +type ContactDataT = { + [key in PlansE]: PlanContactInfoT; +}; const SupportGrid = () => { - const isStarter = useIsStarter(); + const account = useSelector(selectAccount); + const isProfessionalUp = useIsProfessionalUp(); + const [contactData, setContactData] = useState({ + email: '', + calendar: '', + }); + + const contactsData: ContactDataT = useMemo(() => { + const defualtContactData: PlanContactInfoT = { + email: 'mailto:hubs-feedback@mozilla.com', + calendar: '', + }; + return { + business: { + email: 'mailto:hubs-business@mozilla.com', + calendar: 'https://calendly.com/mhmorran/hubs-subscription-support', + }, + professional: { + email: 'mailto:hubs-professional@mozilla.com', + calendar: 'https://calendly.com/mhmorran/hubs-subscription-support', + }, + personal: defualtContactData, + starter: defualtContactData, + standard: defualtContactData, + }; + }, []); + + useEffect(() => { + setContactData(contactsData[account.planName as PlansE]); + }, [contactsData, account]); + return (
- {!isStarter && } - + {isProfessionalUp && }
{ image={mailCircle} // Firefox does not honor "_bank" on "mailtos" onClick={() => { - window.open('mailto:hubs-feedback@mozilla.com'); + window.open(contactData.email); }} body="Contact us via email with your questions." /> diff --git a/client/pages/analytics/index.tsx b/client/pages/analytics/index.tsx index d44ea220..1540cfa3 100644 --- a/client/pages/analytics/index.tsx +++ b/client/pages/analytics/index.tsx @@ -4,7 +4,7 @@ import Card from '@Shared/Card/Card'; import { getAnalytics, HubStat } from 'services/analytics.service'; import { Button, Input, Pill } from '@mozilla/lilypad-ui'; import { useState, ChangeEvent } from 'react'; -import { requireAuthenticationAndSubscription } from 'services/routeGuard.service'; +import { analyticsRG } from 'services/routeGuard.service'; import type { GetServerSidePropsContext } from 'next'; type SandboxPropsT = { @@ -29,7 +29,7 @@ const initTiers = { b0: 0, }; -const Sandbox = ({ analytics }: SandboxPropsT) => { +const Analytics = ({ analytics }: SandboxPropsT) => { // Fromat Date Util const getFormattedDate = () => { const today = new Date(); @@ -367,17 +367,11 @@ const Sandbox = ({ analytics }: SandboxPropsT) => { ); }; -export default Sandbox; +export default Analytics; -export async function getS() {} - -export const getServerSideProps = requireAuthenticationAndSubscription( +export const getServerSideProps = analyticsRG( (context: GetServerSidePropsContext) => { // Your normal `getServerSideProps` code here - if (process.env.ENV === 'production') { - return { notFound: true }; - } - return { props: {}, }; diff --git a/client/pages/subscribe/index.tsx b/client/pages/subscribe/index.tsx index 37c26b1a..005adabf 100644 --- a/client/pages/subscribe/index.tsx +++ b/client/pages/subscribe/index.tsx @@ -8,6 +8,7 @@ import { PersonalPlanCard, StarterPlanCard, ProfessionalPlanCard, + BusinessPlanCard, } from '@Modules/plans/plan-cards'; import { BillingPeriodE } from 'types/General'; @@ -25,18 +26,27 @@ const Subscribe = () => {
-
-

Choose your plan

-
+
+
+

Choose your plan

+
-
- - - - +
+
+ + +
+
+ + +
+
+
+ +
diff --git a/client/pages/subscribe/subscribe.module.scss b/client/pages/subscribe/subscribe.module.scss index a823a0a0..54e8a536 100644 --- a/client/pages/subscribe/subscribe.module.scss +++ b/client/pages/subscribe/subscribe.module.scss @@ -2,8 +2,8 @@ .wrapper { margin-top: 50px; - flex-wrap: wrap; - flex-grow: 1; + display: flex; + justify-content: center; @include mobile-down { margin-top: 40px; @@ -22,9 +22,3 @@ margin: 0 0 40px; } } - -.cards { - display: flex; - justify-content: center; - flex-wrap: wrap; -} diff --git a/client/services/routeGuard.service.ts b/client/services/routeGuard.service.ts index 105bcac3..76ef9450 100644 --- a/client/services/routeGuard.service.ts +++ b/client/services/routeGuard.service.ts @@ -171,14 +171,16 @@ export function customClientRG(gssp: Function): GetServerSideProps | Redirect { return async (context: GetServerSidePropsContext) => { const { req } = context; + // Only these plans can see custom client screen + const approvedPlans = [PlansE.PROFESSIONAL, PlansE.BUSINESS]; + // If no errors user is authenticated try { const account: AccountT = await getAccount( req.headers as AxiosRequestHeaders ); - // only b1 can get to this page - if (account.planName !== PlansE.PROFESSIONAL) { + if (!approvedPlans.includes(account.planName as PlansE)) { return redirectToDashboard(); } @@ -265,6 +267,42 @@ export function SubscribeRG(gssp: Function): GetServerSideProps { }; } +export function analyticsRG(gssp: Function): GetServerSideProps | Redirect { + return async (context: GetServerSidePropsContext) => { + const { req } = context; + + if (didSetTurkeyauthCookie(context)) { + return redirectToDashboard(); + } + + // If no errors user is authenticated + try { + const account: AccountT = await getAccount( + req.headers as AxiosRequestHeaders + ); + + const emails = [ + 'jacobkyle88@gmail.com', + 'mmorran@mozilla.com', + 'ngrato@gmail.com', + 'local-user@turkey.local', + ]; + + // User is authenticated + if (emails.includes(account.email)) { + return await gssp(context); + } + + return redirectToDashboard(); + } catch (error) { + return handleUnauthenticatedRedirects( + error as AxiosError, + req.url as RoutesE + ); + } + }; +} + /************************** * LOCAL DEV UTILITIES **************************/ diff --git a/client/styles/tools/flex.scss b/client/styles/tools/flex.scss index 6e22c2cc..e912d2b0 100644 --- a/client/styles/tools/flex.scss +++ b/client/styles/tools/flex.scss @@ -46,3 +46,8 @@ display: flex; flex-direction: column; } + +.flex-wrap { + display: flex; + flex-wrap: wrap; +} diff --git a/lib/dash/plan_state_machine.ex b/lib/dash/plan_state_machine.ex index a71fef56..73a481ed 100644 --- a/lib/dash/plan_state_machine.ex +++ b/lib/dash/plan_state_machine.ex @@ -13,6 +13,8 @@ defmodule Dash.PlanStateMachine do @personal_storage_limit_mb 2_000 @professional_ccu_limit 50 @professional_storage_limit_mb 25_000 + @business_ccu_limit 400 + @business_storage_limit_mb 200_000 alias Dash.{Account, Hub, Plan, OrchClient, Repo} import Dash.Utils, only: [rand_string: 1] @@ -410,6 +412,128 @@ defmodule Dash.PlanStateMachine do end end + # BUSINESS PLAN + def handle_event( + nil, + {:subscribe_business, %DateTime{} = subscribed_at}, + %Account{} = account, + _data + ) do + %{plan_id: plan_id} = Repo.insert!(%Plan{account_id: account.account_id}) + :ok = subscribe_business(plan_id, subscribed_at) + + hub = + Repo.insert!(%Hub{ + account_id: account.account_id, + ccu_limit: @business_ccu_limit, + status: :creating, + storage_limit_mb: @business_storage_limit_mb, + subdomain: rand_string(10), + tier: :b2 + }) + + {:ok, %{body: json, status_code: 200}} = OrchClient.create_hub(account.email, hub) + :ok = put_domain(hub.hub_id, json) + end + + # EXPIRED SUBSCRIPTION + def handle_event( + :business, + {:expire_subscription, %DateTime{} = expired_at}, + %Account{account_id: account_id, email: email}, + _data + ) do + if DateTime.compare(expired_at, transitioned_at(account_id)) !== :gt do + {:error, :superseded} + else + %{plan_id: plan_id} = fetch_plan!(account_id) + + Repo.insert!(%__MODULE__.PlanTransition{ + event: "expire_subscription", + new_state: :starter, + plan_id: plan_id, + transitioned_at: expired_at + }) + + hub = + Hub + |> Repo.get_by!(account_id: account_id) + |> Repo.preload(:deployment) + |> Ecto.Changeset.change( + ccu_limit: @starter_ccu_limit, + status: :updating, + storage_limit_mb: @starter_storage_limit_mb, + subdomain: rand_string(10), + tier: :p0 + ) + |> Repo.update!() + + {:ok, %{status_code: 200}} = + OrchClient.update_hub(email, hub, + disable_branding?: true, + reset_branding?: true, + reset_client?: true, + reset_domain?: true + ) + + :ok + end + end + + # CHANGES SUBSCRIPTION TO PERSONAL + def handle_event( + :business, + {:subscribe_personal, subscribed_at}, + %Account{account_id: account_id, email: email}, + _data + ) do + if DateTime.compare(subscribed_at, transitioned_at(account_id)) !== :gt do + {:error, :superseded} + else + %{plan_id: plan_id} = fetch_plan!(account_id) + :ok = subscribe_personal(plan_id, subscribed_at) + + hub = + Hub + |> Repo.get_by!(account_id: account_id) + |> Repo.preload(:deployment) + |> Ecto.Changeset.change( + ccu_limit: @personal_ccu_limit, + storage_limit_mb: @personal_storage_limit_mb, + status: :updating, + tier: :p1 + ) + |> Repo.update!() + + {:ok, %{status_code: 200}} = + OrchClient.update_hub(email, hub, reset_client?: true, reset_domain?: true) + + :ok + end + end + + # ATEMPT TO SUBSCRIPT TO BUSINESS AGAIN + def handle_event( + :business, + {:subscribe_professional, %DateTime{} = subscribed_at}, + %Account{account_id: account_id}, + _data + ) do + if DateTime.compare(subscribed_at, transitioned_at(account_id)) !== :gt do + {:error, :superseded} + else + {:error, :already_started} + end + end + + # ALREADY STARTED + def handle_event(:business, :start, %Account{}, _data), + do: {:error, :already_started} + + # FETCH ACTIVE PLAN + def handle_event(:business, :fetch_active_plan, %Account{account_id: account_id}, _data), + do: {:ok, %{fetch_plan!(account_id) | name: "business", subscription?: true}} + @spec fetch_plan!(Account.id()) :: Plan.t() defp fetch_plan!(account_id), do: Repo.get_by!(Plan, account_id: account_id) @@ -453,6 +577,18 @@ defmodule Dash.PlanStateMachine do :ok end + @spec subscribe_business(Plan.id(), DateTime.t()) :: :ok + defp subscribe_business(plan_id, %DateTime{} = subscribed_at) when is_integer(plan_id) do + Repo.insert!(%__MODULE__.PlanTransition{ + event: "subscribe_business", + new_state: :business, + plan_id: plan_id, + transitioned_at: subscribed_at + }) + + :ok + end + @spec transitioned_at(Account.id()) :: DateTime.t() defp transitioned_at(account_id) when is_integer(account_id), do: diff --git a/marketing/components/shared/Subscribe/BasePlanCard/BasePlanCard.module.scss b/marketing/components/shared/Subscribe/BasePlanCard/BasePlanCard.module.scss index 590c2a07..2b49ab8b 100644 --- a/marketing/components/shared/Subscribe/BasePlanCard/BasePlanCard.module.scss +++ b/marketing/components/shared/Subscribe/BasePlanCard/BasePlanCard.module.scss @@ -4,7 +4,6 @@ background-color: $color-background-neutral-0; box-shadow: 0px 0px 16px rgba(0, 0, 0, 0.15); border-radius: 20px; - width: 386px; color: $color-text-main; padding: 40px; overflow: hidden; diff --git a/marketing/components/shared/Subscribe/Subscribe.module.scss b/marketing/components/shared/Subscribe/Subscribe.module.scss index e6e9dfdf..7a979728 100644 --- a/marketing/components/shared/Subscribe/Subscribe.module.scss +++ b/marketing/components/shared/Subscribe/Subscribe.module.scss @@ -46,18 +46,21 @@ &_3 { grid-area: '3'; } + &_4 { + grid-area: '4'; + } } .cards { display: grid; - grid-template-columns: 386px 386px 386px; + grid-template-columns: 288px 288px 288px 288px; grid-template-rows: auto auto; column-gap: 18px; gap: 18px; margin: 0 8px; - grid-template-areas: '1 2 3' '. . hanger'; + grid-template-areas: '1 2 3 4' '. . hanger hanger'; @include desktop-large-down { - grid-template-areas: '1' '2' '3' 'hanger'; + grid-template-areas: '1' '2' '3' '4' 'hanger'; grid-template-columns: 100%; } @include mobile-down { @@ -110,7 +113,6 @@ background-color: $color-background-neutral-0; box-shadow: 0px 0px 16px rgba(0, 0, 0, 0.15); border-radius: 20px; - width: 386px; padding: 30px 24px 24px 24px; overflow: hidden; position: relative; diff --git a/marketing/components/shared/Subscribe/Subscribe.tsx b/marketing/components/shared/Subscribe/Subscribe.tsx index 1cf663fd..88b7c4ea 100644 --- a/marketing/components/shared/Subscribe/Subscribe.tsx +++ b/marketing/components/shared/Subscribe/Subscribe.tsx @@ -6,7 +6,12 @@ import { RegionCodeT } from 'types/Countries'; import { BillingPeriodE, PlansE } from 'types/General'; import { getPricePageData } from 'util/utilities'; import getEnvVariable from 'config'; -import { STARTER_COPY, PERSONAL_COPY, PROFESSIONAL_COPY } from './plan.const'; +import { + STARTER_COPY, + PERSONAL_COPY, + PROFESSIONAL_COPY, + BUSINESS_COPY, +} from './plan.const'; import SkeletonCard from '@Shared/SkeletonCard/SkeletonCard'; import { Modal, Button } from '@mozilla/lilypad-ui'; import Snow from '@Shared/Snow/Snow'; @@ -30,7 +35,7 @@ const Subscribe = ({ classProp = '' }: SubscribePropsT) => { { label: 'Monthly', value: BillingPeriodE.MONTHLY }, { label: 'Annual', value: BillingPeriodE.ANNUAL }, ]; - const PLAN_QTY = 3; + const PLAN_QTY = 4; /** * Init Plans Data */ @@ -47,7 +52,13 @@ const Subscribe = ({ classProp = '' }: SubscribePropsT) => { billingPeriod ); - const loadingCards = isTabletDown ? [1] : [1, 2, 3]; + const businessPlanData = getPricePageData( + regionCode, + PlansE.BUSINESS, + billingPeriod + ); + + const loadingCards = isTabletDown ? [1] : [1, 2, 3, 4]; /** * Init Region Data @@ -233,6 +244,39 @@ const Subscribe = ({ classProp = '' }: SubscribePropsT) => { } /> + {/* BUSINESS_COPY PLAN */} + + } + valueProps={BUSINESS_COPY.valueProps} + features={BUSINESS_COPY.features} + additionalContent={ + + } + confirmButton={ +