feat: manage plan (#1488)

# Description

- add crown on premium user
- link to manage plan in page `/user`

## ⚠️ Before merging

Setup env variable:
```env
NEXT_PUBLIC_STRIPE_MANAGE_PLAN_URL=<ignore-me-or-change-me>
```

## Screenshots (if appropriate):

<img width="290" alt="image"
src="https://github.com/StanGirard/quivr/assets/67386567/a87b0f7e-b07c-4f4e-b9d2-515ac25ebf05">

<img width="318" alt="image"
src="https://github.com/StanGirard/quivr/assets/67386567/6a4f4f72-8c75-45da-9468-cae1a8d28935">
This commit is contained in:
Matthieu Jacq 2023-10-25 16:11:17 +02:00 committed by GitHub
parent 41563767ad
commit d311a53b6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 67 additions and 30 deletions

View File

@ -18,3 +18,4 @@ NEXT_PUBLIC_CMS_URL=https://cms.quivr.app
NEXT_PUBLIC_STRIPE_PRICING_TABLE_ID=<ignore-me-or-change-me> NEXT_PUBLIC_STRIPE_PRICING_TABLE_ID=<ignore-me-or-change-me>
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=<ignore-me-or-change-me> NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=<ignore-me-or-change-me>
NEXT_PUBLIC_STRIPE_MANAGE_PLAN_URL=<ignore-me-or-change-me>

View File

@ -0,0 +1,29 @@
import { useFeatureIsOn } from "@growthbook/growthbook-react";
import { useTranslation } from "react-i18next";
import { StripePricingModal } from "@/lib/components/Stripe";
import Button from "@/lib/components/ui/Button";
import { useUserData } from "@/lib/hooks/useUserData";
const MANAGE_PLAN_URL = process.env.NEXT_PUBLIC_STRIPE_MANAGE_PLAN_URL;
export const StripePricingOrManageButton = (): JSX.Element => {
const { t } = useTranslation("monetization");
const { userData } = useUserData();
const monetizationIsOn = useFeatureIsOn("monetization");
if (!monetizationIsOn) {
return <></>;
}
const is_premium = userData?.is_premium ?? false;
if (is_premium) {
return (
<a href={MANAGE_PLAN_URL} target="_blank" rel="noopener">
<Button className="w-full">{t("manage_plan")}</Button>
</a>
);
}
return <StripePricingModal Trigger={<Button>{t("upgrade")}</Button>} />;
};

View File

@ -1 +1,2 @@
export { UserStatistics } from "./UserStatistics"; export { UserStatistics } from "./UserStatistics";
export { StripePricingOrManageButton } from "./StripePricingOrManageButton";

View File

@ -1,36 +1,26 @@
/* eslint-disable max-lines */
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { StripePricingModal } from "@/lib/components/Stripe";
import Button from "@/lib/components/ui/Button"; import Button from "@/lib/components/ui/Button";
import Card, { CardBody, CardHeader } from "@/lib/components/ui/Card"; import Card, { CardBody, CardHeader } from "@/lib/components/ui/Card";
import { useSupabase } from "@/lib/context/SupabaseProvider"; import { useSupabase } from "@/lib/context/SupabaseProvider";
import { useUserData } from "@/lib/hooks/useUserData";
import { redirectToLogin } from "@/lib/router/redirectToLogin"; import { redirectToLogin } from "@/lib/router/redirectToLogin";
import { UserStatistics } from "./components"; import { StripePricingOrManageButton, UserStatistics } from "./components";
import { ApiKeyConfig } from "./components/ApiKeyConfig"; import { ApiKeyConfig } from "./components/ApiKeyConfig";
import LanguageSelect from "./components/LanguageDropDown/LanguageSelect"; import LanguageSelect from "./components/LanguageDropDown/LanguageSelect";
import ThemeSelect from "./components/ThemeSelect/ThemeSelect"; import ThemeSelect from "./components/ThemeSelect/ThemeSelect";
const UserPage = (): JSX.Element => { const UserPage = (): JSX.Element => {
const { session } = useSupabase(); const { session } = useSupabase();
const { userData } = useUserData();
const is_premium = userData?.is_premium;
if (!session) { if (!session) {
redirectToLogin(); redirectToLogin();
} }
const { user } = session; const { user } = session;
const { t } = useTranslation([ const { t } = useTranslation(["translation", "user", "config"]);
"translation",
"user",
"config",
"monetization",
]);
return ( return (
<main className="container lg:w-2/3 mx-auto py-10 px-5"> <main className="container lg:w-2/3 mx-auto py-10 px-5">
@ -52,11 +42,7 @@ const UserPage = (): JSX.Element => {
</Button> </Button>
</Link> </Link>
</div> </div>
{is_premium === true ? null : ( <StripePricingOrManageButton />
<StripePricingModal
Trigger={<Button>{t("monetization:upgrade")}</Button>}
/>
)}
</CardBody> </CardBody>
</Card> </Card>

View File

@ -27,8 +27,10 @@ export const SidebarFooterButton = ({
className="w-full rounded-lg px-5 py-2 text-base flex justify-start items-center gap-4 hover:bg-gray-200 dark:hover:bg-gray-800 hover:text-primary focus:outline-none" className="w-full rounded-lg px-5 py-2 text-base flex justify-start items-center gap-4 hover:bg-gray-200 dark:hover:bg-gray-800 hover:text-primary focus:outline-none"
onClick={onClick} onClick={onClick}
> >
{icon} <span className="w-8 shrink-0">{icon}</span>
<span className="text-ellipsis overflow-hidden">{label}</span> <span className="w-full text-ellipsis overflow-hidden text-start">
{label}
</span>
</button> </button>
); );
}; };

View File

@ -23,12 +23,12 @@ export const UpgradeToPlus = (): JSX.Element => {
<SidebarFooterButton <SidebarFooterButton
icon={<FiUser className="w-8 h-8" />} icon={<FiUser className="w-8 h-8" />}
label={ label={
<> <div className="flex justify-between items-center w-full">
{t("upgrade")}{" "} {t("upgrade")}
<span className="rounded bg-primary/50 py-1 px-3 text-xs"> <span className="rounded bg-primary/30 py-1 px-3 text-xs">
{t("new")} {t("new")}
</span> </span>
</> </div>
} }
/> />
} }

View File

@ -1,5 +1,8 @@
import { FaCrown } from "react-icons/fa";
import { Avatar } from "@/lib/components/ui/Avatar"; import { Avatar } from "@/lib/components/ui/Avatar";
import { useSupabase } from "@/lib/context/SupabaseProvider"; import { useSupabase } from "@/lib/context/SupabaseProvider";
import { useUserData } from "@/lib/hooks/useUserData";
import { SidebarFooterButton } from "./SidebarFooterButton"; import { SidebarFooterButton } from "./SidebarFooterButton";
import { useGravatar } from "../../../../../hooks/useGravatar"; import { useGravatar } from "../../../../../hooks/useGravatar";
@ -7,12 +10,21 @@ import { useGravatar } from "../../../../../hooks/useGravatar";
export const UserButton = (): JSX.Element => { export const UserButton = (): JSX.Element => {
const { session } = useSupabase(); const { session } = useSupabase();
const { gravatarUrl } = useGravatar(); const { gravatarUrl } = useGravatar();
const { userData } = useUserData();
const is_premium = userData?.is_premium ?? false;
const email = session?.user.email ?? "";
const label = (
<span className="flex justify-between items-center flex-nowrap gap-1 w-full">
<span className="text-ellipsis overflow-hidden">{email}</span>
{is_premium && <FaCrown className="w-5 h-5 shrink-0" />}
</span>
);
return ( return (
<SidebarFooterButton <SidebarFooterButton
href={"/user"} href={"/user"}
icon={<Avatar url={gravatarUrl} />} icon={<Avatar url={gravatarUrl} />}
label={session?.user.email ?? ""} label={label}
/> />
); );
}; };

View File

@ -1,4 +1,5 @@
{ {
"upgrade": "Upgrade to plus", "upgrade": "Upgrade to plus",
"new": "New" "new": "New",
"manage_plan": "Manage my plan"
} }

View File

@ -1,4 +1,5 @@
{ {
"upgrade": "Actualizar a plus", "upgrade": "Actualizar a plus",
"new": "Nuevo" "new": "Nuevo",
"manage_plan": "Gestionar mi plan"
} }

View File

@ -1,4 +1,5 @@
{ {
"upgrade": "Passer à la version plus", "upgrade": "Passer à la version plus",
"new": "Nouveau" "new": "Nouveau",
"manage_plan": "Gérer mon plan"
} }

View File

@ -1,4 +1,5 @@
{ {
"upgrade": "Atualizar para o plus", "upgrade": "Atualizar para o plus",
"new": "Novo" "new": "Novo",
"manage_plan": "Gerenciar meu plano"
} }

View File

@ -1,4 +1,5 @@
{ {
"upgrade": "Обновить до плюса", "upgrade": "Обновить до плюса",
"new": "Новый" "new": "Новый",
"manage_plan": "Управлять моим планом"
} }

View File

@ -1,4 +1,5 @@
{ {
"upgrade": "升级至高级版", "upgrade": "升级至高级版",
"new": "新" "new": "新",
"manage_plan": "管理我的计划"
} }