feat: upgrade to plus button (#1482)

# Description

Epic: #1429 
User Story: #1430 

## Pour la mise en preview / prod:

- Mettre à jour l'environnement

```env
NEXT_PUBLIC_STRIPE_PRICING_TABLE_ID=<change-me>
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=<change-me>
```

- Activer le feature flag `monetization` (booléen)

## Screenshots (if appropriate):

Button:
<img width="289" alt="image"
src="https://github.com/StanGirard/quivr/assets/67386567/c0f7321e-2f48-4462-aab9-fd1c6f4282cd">

Modal:
<img width="843" alt="image"
src="https://github.com/StanGirard/quivr/assets/67386567/28082680-1126-44db-bf77-76ae7474747f">
This commit is contained in:
Matthieu Jacq 2023-10-24 18:26:48 +02:00 committed by GitHub
parent 0bf7d36629
commit 56d1f94b62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 137 additions and 8 deletions

View File

@ -14,4 +14,7 @@ NEXT_PUBLIC_E2E_URL=http://localhost:3003
NEXT_PUBLIC_E2E_EMAIL=<ignore-me-or-change-me> NEXT_PUBLIC_E2E_EMAIL=<ignore-me-or-change-me>
NEXT_PUBLIC_E2E_PASSWORD=<ignore-me-or-change-me> NEXT_PUBLIC_E2E_PASSWORD=<ignore-me-or-change-me>
NEXT_PUBLIC_CMS_URL=https://cms.quivr.app NEXT_PUBLIC_CMS_URL=https://cms.quivr.app
NEXT_PUBLIC_STRIPE_PRICING_TABLE_ID=<ignore-me-or-change-me>
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=<ignore-me-or-change-me>

View File

@ -17,7 +17,7 @@ export const BrainsList = (): JSX.Element => {
const { t } = useTranslation(["brain", "chat"]); const { t } = useTranslation(["brain", "chat"]);
return ( return (
<Sidebar showButtons={["user"]}> <Sidebar showButtons={["user", "upgradeToPlus"]}>
<div className="flex flex-col p-2 gap-2"> <div className="flex flex-col p-2 gap-2">
<Link href="/chat"> <Link href="/chat">
<Button type="button" className="bg-primary text-white py-2 w-full"> <Button type="button" className="bg-primary text-white py-2 w-full">

View File

@ -15,7 +15,7 @@ export const ChatsList = (): JSX.Element => {
const { shouldDisplayWelcomeChat } = useOnboarding(); const { shouldDisplayWelcomeChat } = useOnboarding();
return ( return (
<Sidebar showButtons={["myBrains", "user"]}> <Sidebar showButtons={["myBrains", "user", "upgradeToPlus"]}>
<div className="flex flex-col flex-1 h-full" data-testid="chats-list"> <div className="flex flex-col flex-1 h-full" data-testid="chats-list">
<div className="pt-2"> <div className="pt-2">
<NewChatButton /> <NewChatButton />

View File

@ -1,19 +1,23 @@
import { BrainManagementButton } from "@/lib/components/Sidebar/components/SidebarFooter/components/BrainManagementButton"; import { BrainManagementButton } from "@/lib/components/Sidebar/components/SidebarFooter/components/BrainManagementButton";
import { UpgradeToPlus } from "./components/UpgradeToPlus";
import { UserButton } from "./components/UserButton"; import { UserButton } from "./components/UserButton";
export type SidebarFooterButtons = "myBrains" | "user"; export type SidebarFooterButtons = "myBrains" | "user" | "upgradeToPlus";
type SidebarFooterProps = { type SidebarFooterProps = {
showButtons: SidebarFooterButtons[]; showButtons: SidebarFooterButtons[];
}; };
export const SidebarFooter = ({showButtons}: SidebarFooterProps): JSX.Element => { export const SidebarFooter = ({
showButtons,
}: SidebarFooterProps): JSX.Element => {
return ( return (
<div className="bg-gray-50 dark:bg-gray-900 border-t dark:border-white/10 mt-auto p-2"> <div className="bg-gray-50 dark:bg-gray-900 border-t dark:border-white/10 mt-auto p-2">
<div className="max-w-screen-xl flex justify-center items-center flex-col"> <div className="max-w-screen-xl flex justify-center items-center flex-col">
{showButtons.includes('myBrains') && <BrainManagementButton />} {showButtons.includes("myBrains") && <BrainManagementButton />}
{showButtons.includes('user') && <UserButton />} {showButtons.includes("upgradeToPlus") && <UpgradeToPlus />}
{showButtons.includes("user") && <UserButton />}
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,35 @@
import { useFeatureIsOn } from "@growthbook/growthbook-react";
import { useTranslation } from "react-i18next";
import { FiUser } from "react-icons/fi";
import { StripePricingModal } from "@/lib/components/Stripe";
import { useUserData } from "@/lib/hooks/useUserData";
import { sidebarLinkStyle } from "../styles/SidebarLinkStyle";
export const UpgradeToPlus = (): JSX.Element => {
const { userData } = useUserData();
const is_premium = userData?.is_premium;
const featureIsOn = useFeatureIsOn("monetization");
const { t } = useTranslation("monetization");
if (!featureIsOn || is_premium === true) {
return <></>;
}
return (
<StripePricingModal
Trigger={
<button type="button" className={sidebarLinkStyle}>
<FiUser className="w-8 h-8" />
<span>
{t("upgrade")}{" "}
<span className="rounded bg-primary/50 py-1 px-3 text-xs">
{t("new")}
</span>
</span>
</button>
}
/>
);
};

View File

@ -0,0 +1,16 @@
import { StripePricingTable } from "./components/PricingTable/PricingTable";
import { Modal } from "../../ui/Modal";
type StripePricingModalProps = {
Trigger: JSX.Element;
};
export const StripePricingModal = ({
Trigger,
}: StripePricingModalProps): JSX.Element => {
return (
<Modal Trigger={Trigger} CloseTrigger={<div />}>
<StripePricingTable />
</Modal>
);
};

View File

@ -0,0 +1,14 @@
const PRICING_TABLE_ID = process.env.NEXT_PUBLIC_STRIPE_PRICING_TABLE_ID;
const PUBLISHABLE_KEY = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY;
export const StripePricingTable = (): JSX.Element => {
return (
<>
<script async src="https://js.stripe.com/v3/pricing-table.js"></script>
<stripe-pricing-table
pricing-table-id={PRICING_TABLE_ID}
publishable-key={PUBLISHABLE_KEY}
></stripe-pricing-table>
</>
);
};

View File

@ -0,0 +1,12 @@
import * as React from "react";
declare global {
namespace JSX {
interface IntrinsicElements {
"stripe-pricing-table": React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement>,
HTMLElement
>;
}
}
}

View File

@ -0,0 +1 @@
export * from "./PricingModal/StripePricingModal";

View File

@ -11,6 +11,7 @@ import invitation_en from "../../../public/locales/en/invitation.json";
import knowlegde_en from "../../../public/locales/en/knowledge.json"; import knowlegde_en from "../../../public/locales/en/knowledge.json";
import login_en from "../../../public/locales/en/login.json"; import login_en from "../../../public/locales/en/login.json";
import logout_en from "../../../public/locales/en/logout.json"; import logout_en from "../../../public/locales/en/logout.json";
import monetization_en from "../../../public/locales/en/monetization.json";
import signUp_en from "../../../public/locales/en/signUp.json"; import signUp_en from "../../../public/locales/en/signUp.json";
import translation_en from "../../../public/locales/en/translation.json"; import translation_en from "../../../public/locales/en/translation.json";
import updatePassword_en from "../../../public/locales/en/updatePassword.json"; import updatePassword_en from "../../../public/locales/en/updatePassword.json";
@ -28,6 +29,7 @@ import invitation_es from "../../../public/locales/es/invitation.json";
import knowlegde_es from "../../../public/locales/es/knowledge.json"; import knowlegde_es from "../../../public/locales/es/knowledge.json";
import login_es from "../../../public/locales/es/login.json"; import login_es from "../../../public/locales/es/login.json";
import logout_es from "../../../public/locales/es/logout.json"; import logout_es from "../../../public/locales/es/logout.json";
import monetization_es from "../../../public/locales/es/monetization.json";
import signUp_es from "../../../public/locales/es/signUp.json"; import signUp_es from "../../../public/locales/es/signUp.json";
import translation_es from "../../../public/locales/es/translation.json"; import translation_es from "../../../public/locales/es/translation.json";
import updatePassword_es from "../../../public/locales/es/updatePassword.json"; import updatePassword_es from "../../../public/locales/es/updatePassword.json";
@ -45,6 +47,7 @@ import invitation_fr from "../../../public/locales/fr/invitation.json";
import knowlegde_fr from "../../../public/locales/fr/knowledge.json"; import knowlegde_fr from "../../../public/locales/fr/knowledge.json";
import login_fr from "../../../public/locales/fr/login.json"; import login_fr from "../../../public/locales/fr/login.json";
import logout_fr from "../../../public/locales/fr/logout.json"; import logout_fr from "../../../public/locales/fr/logout.json";
import monetization_fr from "../../../public/locales/fr/monetization.json";
import signUp_fr from "../../../public/locales/fr/signUp.json"; import signUp_fr from "../../../public/locales/fr/signUp.json";
import translation_fr from "../../../public/locales/fr/translation.json"; import translation_fr from "../../../public/locales/fr/translation.json";
import updatePassword_fr from "../../../public/locales/fr/updatePassword.json"; import updatePassword_fr from "../../../public/locales/fr/updatePassword.json";
@ -62,6 +65,7 @@ import invitation_ptbr from "../../../public/locales/pt-br/invitation.json";
import knowlegde_ptbr from "../../../public/locales/pt-br/knowledge.json"; import knowlegde_ptbr from "../../../public/locales/pt-br/knowledge.json";
import login_ptbr from "../../../public/locales/pt-br/login.json"; import login_ptbr from "../../../public/locales/pt-br/login.json";
import logout_ptbr from "../../../public/locales/pt-br/logout.json"; import logout_ptbr from "../../../public/locales/pt-br/logout.json";
import monetization_ptbr from "../../../public/locales/pt-br/monetization.json";
import signUp_ptbr from "../../../public/locales/pt-br/signUp.json"; import signUp_ptbr from "../../../public/locales/pt-br/signUp.json";
import translation_ptbr from "../../../public/locales/pt-br/translation.json"; import translation_ptbr from "../../../public/locales/pt-br/translation.json";
import updatePassword_ptbr from "../../../public/locales/pt-br/updatePassword.json"; import updatePassword_ptbr from "../../../public/locales/pt-br/updatePassword.json";
@ -79,6 +83,7 @@ import invitation_ru from "../../../public/locales/ru/invitation.json";
import knowlegde_ru from "../../../public/locales/ru/knowledge.json"; import knowlegde_ru from "../../../public/locales/ru/knowledge.json";
import login_ru from "../../../public/locales/ru/login.json"; import login_ru from "../../../public/locales/ru/login.json";
import logout_ru from "../../../public/locales/ru/logout.json"; import logout_ru from "../../../public/locales/ru/logout.json";
import monetization_ru from "../../../public/locales/ru/monetization.json";
import signUp_ru from "../../../public/locales/ru/signUp.json"; import signUp_ru from "../../../public/locales/ru/signUp.json";
import translation_ru from "../../../public/locales/ru/translation.json"; import translation_ru from "../../../public/locales/ru/translation.json";
import updatePassword_ru from "../../../public/locales/ru/updatePassword.json"; import updatePassword_ru from "../../../public/locales/ru/updatePassword.json";
@ -96,6 +101,7 @@ import invitation_zh_cn from "../../../public/locales/zh-cn/invitation.json";
import knowlegde_zh_cn from "../../../public/locales/zh-cn/knowledge.json"; import knowlegde_zh_cn from "../../../public/locales/zh-cn/knowledge.json";
import login_zh_cn from "../../../public/locales/zh-cn/login.json"; import login_zh_cn from "../../../public/locales/zh-cn/login.json";
import logout_zh_cn from "../../../public/locales/zh-cn/logout.json"; import logout_zh_cn from "../../../public/locales/zh-cn/logout.json";
import monetization_zh_cn from "../../../public/locales/zh-cn/monetization.json";
import signUp_zh_cn from "../../../public/locales/zh-cn/signUp.json"; import signUp_zh_cn from "../../../public/locales/zh-cn/signUp.json";
import translation_zh_cn from "../../../public/locales/zh-cn/translation.json"; import translation_zh_cn from "../../../public/locales/zh-cn/translation.json";
import updatePassword_zh_cn from "../../../public/locales/zh-cn/updatePassword.json"; import updatePassword_zh_cn from "../../../public/locales/zh-cn/updatePassword.json";
@ -114,6 +120,7 @@ export type Translations = {
invitation: typeof import("../../../public/locales/en/invitation.json"); invitation: typeof import("../../../public/locales/en/invitation.json");
login: typeof import("../../../public/locales/en/login.json"); login: typeof import("../../../public/locales/en/login.json");
logout: typeof import("../../../public/locales/en/logout.json"); logout: typeof import("../../../public/locales/en/logout.json");
monetization: typeof import("../../../public/locales/en/monetization.json");
signUp: typeof import("../../../public/locales/en/signUp.json"); signUp: typeof import("../../../public/locales/en/signUp.json");
translation: typeof import("../../../public/locales/en/translation.json"); translation: typeof import("../../../public/locales/en/translation.json");
updatePassword: typeof import("../../../public/locales/en/updatePassword.json"); updatePassword: typeof import("../../../public/locales/en/updatePassword.json");
@ -143,6 +150,7 @@ export const resources: Record<SupportedLanguages, Translations> = {
invitation: invitation_en, invitation: invitation_en,
login: login_en, login: login_en,
logout: logout_en, logout: logout_en,
monetization: monetization_en,
signUp: signUp_en, signUp: signUp_en,
translation: translation_en, translation: translation_en,
updatePassword: updatePassword_en, updatePassword: updatePassword_en,
@ -161,6 +169,7 @@ export const resources: Record<SupportedLanguages, Translations> = {
invitation: invitation_es, invitation: invitation_es,
login: login_es, login: login_es,
logout: logout_es, logout: logout_es,
monetization: monetization_es,
signUp: signUp_es, signUp: signUp_es,
translation: translation_es, translation: translation_es,
updatePassword: updatePassword_es, updatePassword: updatePassword_es,
@ -179,6 +188,7 @@ export const resources: Record<SupportedLanguages, Translations> = {
invitation: invitation_fr, invitation: invitation_fr,
login: login_fr, login: login_fr,
logout: logout_fr, logout: logout_fr,
monetization: monetization_fr,
signUp: signUp_fr, signUp: signUp_fr,
translation: translation_fr, translation: translation_fr,
updatePassword: updatePassword_fr, updatePassword: updatePassword_fr,
@ -197,6 +207,7 @@ export const resources: Record<SupportedLanguages, Translations> = {
invitation: invitation_ptbr, invitation: invitation_ptbr,
login: login_ptbr, login: login_ptbr,
logout: logout_ptbr, logout: logout_ptbr,
monetization: monetization_ptbr,
signUp: signUp_ptbr, signUp: signUp_ptbr,
translation: translation_ptbr, translation: translation_ptbr,
updatePassword: updatePassword_ptbr, updatePassword: updatePassword_ptbr,
@ -215,6 +226,7 @@ export const resources: Record<SupportedLanguages, Translations> = {
invitation: invitation_ru, invitation: invitation_ru,
login: login_ru, login: login_ru,
logout: logout_ru, logout: logout_ru,
monetization: monetization_ru,
signUp: signUp_ru, signUp: signUp_ru,
translation: translation_ru, translation: translation_ru,
updatePassword: updatePassword_ru, updatePassword: updatePassword_ru,
@ -233,6 +245,7 @@ export const resources: Record<SupportedLanguages, Translations> = {
invitation: invitation_zh_cn, invitation: invitation_zh_cn,
login: login_zh_cn, login: login_zh_cn,
logout: logout_zh_cn, logout: logout_zh_cn,
monetization: monetization_zh_cn,
signUp: signUp_zh_cn, signUp: signUp_zh_cn,
translation: translation_zh_cn, translation: translation_zh_cn,
updatePassword: updatePassword_zh_cn, updatePassword: updatePassword_zh_cn,

View File

@ -37,7 +37,12 @@ const ContentSecurityPolicy = {
"https://cdn.growthbook.io", "https://cdn.growthbook.io",
"https://vitals.vercel-insights.com/v1/vitals", "https://vitals.vercel-insights.com/v1/vitals",
], ],
"img-src": ["'self'", "https://www.gravatar.com","https://quivr-cms.s3.eu-west-3.amazonaws.com", "data:"], "img-src": [
"'self'",
"https://www.gravatar.com",
"https://quivr-cms.s3.eu-west-3.amazonaws.com",
"data:",
],
"media-src": [ "media-src": [
"'self'", "'self'",
"https://user-images.githubusercontent.com", "https://user-images.githubusercontent.com",
@ -50,7 +55,9 @@ const ContentSecurityPolicy = {
"https://va.vercel-scripts.com/", "https://va.vercel-scripts.com/",
process.env.NEXT_PUBLIC_FRONTEND_URL, process.env.NEXT_PUBLIC_FRONTEND_URL,
"https://www.google-analytics.com/", "https://www.google-analytics.com/",
"https://js.stripe.com",
], ],
"frame-src": ["https://js.stripe.com"],
"frame-ancestors": ["'none'"], "frame-ancestors": ["'none'"],
"style-src": ["'unsafe-inline'", process.env.NEXT_PUBLIC_FRONTEND_URL], "style-src": ["'unsafe-inline'", process.env.NEXT_PUBLIC_FRONTEND_URL],
}; };

View File

@ -0,0 +1,4 @@
{
"upgrade": "Upgrade to plus",
"new": "New"
}

View File

@ -0,0 +1,4 @@
{
"upgrade": "Actualizar a plus",
"new": "Nuevo"
}

View File

@ -0,0 +1,4 @@
{
"upgrade": "Passer à la version plus",
"new": "Nouveau"
}

View File

@ -0,0 +1,4 @@
{
"upgrade": "Atualizar para o plus",
"new": "Novo"
}

View File

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

View File

@ -0,0 +1,4 @@
{
"upgrade": "升级至高级版",
"new": "新"
}