diff --git a/.github/workflows/firebase-dev-hosting-merge.yml b/.github/workflows/firebase-dev-hosting-merge.yml index 4ab4cf18..d7a6342b 100644 --- a/.github/workflows/firebase-dev-hosting-merge.yml +++ b/.github/workflows/firebase-dev-hosting-merge.yml @@ -23,6 +23,7 @@ env: # Stripe VITE_PRO_PRICE_ID: ${{ secrets.VITE_PRO_PRICE_ID_DEV }} + VITE_PRO_PRICE_YEARLY_ID: ${{ secrets.VITE_PRO_PRICE_YEARLY_ID_DEV }} jobs: build_and_deploy: diff --git a/.github/workflows/firebase-hosting-merge.yml b/.github/workflows/firebase-hosting-merge.yml index 19f18f13..b4b63702 100644 --- a/.github/workflows/firebase-hosting-merge.yml +++ b/.github/workflows/firebase-hosting-merge.yml @@ -21,6 +21,7 @@ env: # Stripe VITE_PRO_PRICE_ID: ${{ secrets.VITE_PRO_PRICE_ID }} + VITE_PRO_PRICE_YEARLY_ID: ${{ secrets.VITE_PRO_PRICE_YEARLY_ID }} jobs: build_and_deploy: diff --git a/.github/workflows/firebase-hosting-pull-request.yml b/.github/workflows/firebase-hosting-pull-request.yml index 93437e6d..db8af9d2 100644 --- a/.github/workflows/firebase-hosting-pull-request.yml +++ b/.github/workflows/firebase-hosting-pull-request.yml @@ -21,6 +21,7 @@ env: # Stripe VITE_PRO_PRICE_ID: ${{ secrets.VITE_PRO_PRICE_ID_DEV }} + VITE_PRO_PRICE_YEARLY_ID: ${{ secrets.VITE_PRO_PRICE_YEARLY_ID_DEV }} jobs: build_and_preview: diff --git a/src/components/premium/PlansList.tsx b/src/components/premium/PlansList.tsx index 68ce20f5..1e943716 100644 --- a/src/components/premium/PlansList.tsx +++ b/src/components/premium/PlansList.tsx @@ -1,250 +1,285 @@ import { CheckIcon, SparklesIcon } from "@heroicons/react/20/solid"; import clsx from "clsx"; -import { FC, useEffect, useMemo, useState } from "react"; +import { FC, useEffect, useState } from "react"; import { logAnalyticsEvent } from "../../services/firestore/analytics/analytics"; -import { Subscription, useCheckoutSessionUrl } from "../../services/stripe"; +import { useCheckoutSessionUrl } from "../../services/stripe"; import { Colors } from "../../utils/colors"; import Badge from "../common/Badge"; +import SubscriptionPeriodToggle, { + SubscriptionPeriodMode, + SubscriptionPeriodModes, +} from "./SubscriptionPeriodToggle"; export interface PlanProps { - isPro: boolean; + isPro: boolean; } const DiscoverPlan: FC = ({ isPro }) => { - return ( -
-

- Discover - {!isPro && ( - - )} -

-

- Great for beginner investigators to satisfy their curiosity. -

-

- Free -

-
-
- - - Graph searching - - - 1 Automation - - - - Limited advanced analysis - - - - Limited AI usage - -
-
- ); + return ( +
+

+ Discover + {!isPro && ( + + )} +

+

+ Great for beginner investigators to satisfy their curiosity. +

+

+ Free +

+
+
+ + + Graph searching + + + 2 Graphs + + + 1 Automation + + + + Limited advanced analysis + + + + Limited AI usage + +
+
+ ); }; interface ProPlanProps extends PlanProps { - userID: string; - subscription?: Subscription; - successRedirectPath?: string; - cancelRedirectPath?: string; + userID: string; + subscriptionPeriodMode: SubscriptionPeriodMode; + successRedirectPath?: string; + cancelRedirectPath?: string; } const ProPlan: FC = ({ - isPro, - userID, - subscription, - successRedirectPath, - cancelRedirectPath, + isPro, + userID, + subscriptionPeriodMode, + successRedirectPath, + cancelRedirectPath, }) => { - const [buyPlanClicked, setBuyPlanClicked] = useState(false); + const [buyPlanClicked, setBuyPlanClicked] = useState(false); - const priceId = useMemo( - () => (subscription ? subscription.price.id : import.meta.env.VITE_PRO_PRICE_ID), - [subscription], - ); + const { + url: checkoutSessionUrl, + loading: isLoadingCheckoutSession, + refetch: getCheckoutSession, + } = useCheckoutSessionUrl(subscriptionPeriodMode.priceId, userID, { + enabled: false, + successPath: successRedirectPath, + cancelPath: cancelRedirectPath, + }); - const { - url: checkoutSessionUrl, - loading: isLoadingCheckoutSession, - refetch: getCheckoutSession, - } = useCheckoutSessionUrl(priceId, userID, { enabled: false, successPath: successRedirectPath, cancelPath: cancelRedirectPath }); + useEffect(() => { + if (buyPlanClicked) { + logAnalyticsEvent("buy_pro_plan_clicked"); + getCheckoutSession(); + setBuyPlanClicked(false); + } + }, [buyPlanClicked]); - useEffect(() => { - if (buyPlanClicked) { - logAnalyticsEvent("buy_pro_plan_clicked"); - getCheckoutSession(); - setBuyPlanClicked(false); - } - }, [buyPlanClicked]); + useEffect(() => { + if (checkoutSessionUrl) { + window.location.href = checkoutSessionUrl; + } + }, [checkoutSessionUrl]); - useEffect(() => { - if (checkoutSessionUrl) { - window.location.href = checkoutSessionUrl; - } - }, [checkoutSessionUrl]); - - return ( -
-

- Pro - {isPro && ( - - )} -

-

- Speed up your investigations and take them to the next level. -

-

- €99.99 - /mo - - - - - - - - - -

- {isPro ? ( -
- ) : ( - - )} -
- - - Multiple graph saving - - - - Unlimited AI usage - - - - Advanced analysis - - - - Automations - - - - - Monitoring Dashboard - - - - - API Access - - -
+ return ( +
+
+

+ Pro + {isPro && ( + + )} +

+ {subscriptionPeriodMode.name === SubscriptionPeriodModes[1].name && ( + + (Billed yearly) + + )} +
+

+ Speed up your investigations and take them to the next level. +

+

+

+ + {`${subscriptionPeriodMode.priceCurrency}${subscriptionPeriodMode.priceValueMonth}`} + /mo + + {subscriptionPeriodMode.name === SubscriptionPeriodModes[1].name && ( + + )}
- ); + + + + + + + + + +

+ {isPro ? ( +
+ ) : ( + + )} +
+ + + Infinite graphs + + + + Unlimited AI usage + + + + Advanced analysis + + + + Automations + + + + + Monitoring Dashboard + + + + + API Access + + +
+
+ ); }; const EnterprisePlan: FC = () => { - return ( -
-

- Enterprise -

-

- Tailormade to your needs. -

-

- Custom -

-
logAnalyticsEvent("enterprise_plan_contact_us_button_clicked")}> - < a - className="bg-white-500 mt-4 flex h-10 w-full flex-row items-center justify-center rounded-md p-5 align-middle text-gray-600 shadow-sm ring-1 ring-inset ring-gray-200 transition-all duration-150 hover:bg-gray-50" - href="https://exqt63fqk9e.typeform.com/to/Oa827WkM" - target="_blank" - > - Contact Us - -
-
- - - Tailormade features - - - - Custom automations - - - - Dedicated support - - - - API integrations - -
-
- ); + return ( +
+

+ Enterprise +

+

+ Tailormade to your needs. +

+

+ Custom +

+
+ logAnalyticsEvent("enterprise_plan_contact_us_button_clicked") + } + > + + Contact Us + +
+
+ + + Tailormade features + + + + Custom automations + + + + Dedicated support + + + + API integrations + +
+
+ ); }; interface PlanListProps { - isPro: boolean; - userID: string; - subscription?: Subscription; - successRedirectPath?: string; - cancelRedirectPath?: string; + isPro: boolean; + userID: string; + successRedirectPath?: string; + cancelRedirectPath?: string; } const PlansList: FC = ({ - isPro, - userID, - subscription, - successRedirectPath, - cancelRedirectPath, + isPro, + userID, + successRedirectPath, + cancelRedirectPath, }) => { - return ( - - <> - - - - - - ) -} + const [subscriptionPeriodMode, setSubscriptionPeriodMode] = + useState(SubscriptionPeriodModes[1]); + + return ( + <> +
+ +
+
+ <> + + + + +
+ + ); +}; -export { - DiscoverPlan, - ProPlan, - EnterprisePlan, - PlansList, -}; \ No newline at end of file +export { DiscoverPlan, EnterprisePlan, PlansList, ProPlan }; diff --git a/src/components/premium/SubscriptionPeriodToggle.tsx b/src/components/premium/SubscriptionPeriodToggle.tsx new file mode 100644 index 00000000..3cd83d13 --- /dev/null +++ b/src/components/premium/SubscriptionPeriodToggle.tsx @@ -0,0 +1,88 @@ +import { ArrowRightIcon } from "@heroicons/react/20/solid"; +import clsx from "clsx"; +import { FC } from "react"; + +interface SubscriptionPeriodButtonProps { + isActive: boolean; + subscriptionPeriodMode: SubscriptionPeriodMode; + setSubscriptionPeriodMode: (mode: SubscriptionPeriodMode) => void; +} + +export enum SubscriptionPeriodNames { + MONTHLY = "Monthly", + YEARLY = "Yearly", +} + +export interface SubscriptionPeriodMode { + name: SubscriptionPeriodNames; + priceId: string; + priceValueMonth: number; + priceCurrency: string; +} + +export const SubscriptionPeriodModes: SubscriptionPeriodMode[] = [ + { + name: SubscriptionPeriodNames.MONTHLY, + priceId: import.meta.env.VITE_PRO_PRICE_ID as string, + priceValueMonth: 99.99, + priceCurrency: "€", + }, + { + name: SubscriptionPeriodNames.YEARLY, + priceId: import.meta.env.VITE_PRO_PRICE_YEARLY_ID as string, + priceValueMonth: 83.33, + priceCurrency: "€", + }, +]; + +const SubscriptionPeriodButton: FC = ({ + isActive, + subscriptionPeriodMode, + setSubscriptionPeriodMode, +}) => { + return ( + + ); +}; + +interface SubscriptionPeriodToggleProps { + subscriptionPeriodMode: SubscriptionPeriodMode; + setSubscriptionPeriodMode: (mode: SubscriptionPeriodMode) => void; +} + +const SubscriptionPeriodToggle: FC = ({ + subscriptionPeriodMode, + setSubscriptionPeriodMode, +}) => { + return ( + + {SubscriptionPeriodModes.map((mode) => ( + + ))} + + ); +}; + +export default SubscriptionPeriodToggle; diff --git a/src/templates/BillingTemplate.tsx b/src/templates/BillingTemplate.tsx index db30f883..6257ecc0 100644 --- a/src/templates/BillingTemplate.tsx +++ b/src/templates/BillingTemplate.tsx @@ -1,19 +1,16 @@ import { CreditCardIcon as CreditCardSmallIcon, - RocketLaunchIcon + RocketLaunchIcon, } from "@heroicons/react/20/solid"; import { CreditCardIcon } from "@heroicons/react/24/outline"; import { FC, useEffect, useMemo, useState } from "react"; import BigButton from "../components/common/BigButton"; +import SEO from "../components/common/SEO"; +import { PlansList } from "../components/premium"; import useAuthState from "../hooks/useAuthState"; import { logAnalyticsEvent } from "../services/firestore/analytics/analytics"; import { usePremiumStatus } from "../services/firestore/user/premium/premium"; -import { - useActiveSubscription, - useCustomerPortalUrl -} from "../services/stripe"; -import { PlansList } from "../components/premium"; -import SEO from "../components/common/SEO"; +import { useCustomerPortalUrl } from "../services/stripe"; const BillingTemplate: FC = () => { const [manageSubscriptionClicked, setManageSubscriptionClicked] = @@ -31,18 +28,18 @@ const BillingTemplate: FC = () => { refetch: getCustomerPortalUrl, } = useCustomerPortalUrl(userID, { enabled: false }); - const { subscription, loading: isLoadingActiveSubscription } = - useActiveSubscription(); + /* const { subscription, loading: isLoadingActiveSubscription } = + useActiveSubscription(); */ const isLoading = useMemo(() => { return ( isLoadingPremiumStatus || - isLoadingActiveSubscription || + //isLoadingActiveSubscription || isLoadingCustomerPortalUrl ); }, [ isLoadingPremiumStatus, - isLoadingActiveSubscription, + //isLoadingActiveSubscription, isLoadingCustomerPortalUrl, ]); @@ -64,47 +61,39 @@ const BillingTemplate: FC = () => { <>
-

+

Plan & Billing

-
- - { - // TODO: Replace for a loading component to standardize the loading state - <> - -

- Plan: - {isLoading ? ( -
- ) : isPro ? ( -

- - Pro -

- ) : ( -

Free

- )} -

- {isPro && ( - setManageSubscriptionClicked(true)} - Icon={CreditCardSmallIcon} - /> - )} -
- +

+ Plan: + {isLoading ? ( +
+ ) : isPro ? ( +

+ + Pro +

+ ) : ( +

Free

+ )} +

+ {isPro && ( + setManageSubscriptionClicked(true)} + Icon={CreditCardSmallIcon} /> - - } -
+ )} + + +
); };