diff --git a/apps/renderer/src/lib/api-fetch.ts b/apps/renderer/src/lib/api-fetch.ts index d8ea316fd..e54b93618 100644 --- a/apps/renderer/src/lib/api-fetch.ts +++ b/apps/renderer/src/lib/api-fetch.ts @@ -4,9 +4,12 @@ import { getCsrfToken } from "@hono/auth-js/react" import PKG from "@pkg" import { hc } from "hono/client" import { FetchError, ofetch } from "ofetch" +import { createElement } from "react" +import { toast } from "sonner" import { NetworkStatus, setApiStatus } from "~/atoms/network" import { setLoginModalShow } from "~/atoms/user" +import { NeedActivationToast } from "~/modules/activation/NeedActivationToast" let csrfTokenPromise: Promise | null = null export const apiFetch = ofetch.create({ @@ -63,6 +66,20 @@ export const apiFetch = ofetch.create({ if (context.response.status === 400 && json.code === 1003) { router.navigate("/invitation") } + if (json.code.toString().startsWith("11")) { + setTimeout(() => { + const toastId = toast.error( + createElement(NeedActivationToast, { + dimiss: () => { + toast.dismiss(toastId) + }, + }), + { + duration: 10e4, + }, + ) + }, 500) + } } catch { // ignore } diff --git a/apps/renderer/src/modules/activation/ActivationModalContent.tsx b/apps/renderer/src/modules/activation/ActivationModalContent.tsx new file mode 100644 index 000000000..63f6ce9d7 --- /dev/null +++ b/apps/renderer/src/modules/activation/ActivationModalContent.tsx @@ -0,0 +1,113 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { DotLottieReact } from "@lottiefiles/dotlottie-react" +import { m } from "framer-motion" +import { useEffect, useState } from "react" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import { z } from "zod" + +import { Button } from "~/components/ui/button" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "~/components/ui/form" +import { Input } from "~/components/ui/input" +import { useCurrentModal } from "~/components/ui/modal" +import { getFetchErrorMessage } from "~/lib/error-parser" +import confettiUrl from "~/lottie/confetti.lottie?url" +import { auth } from "~/queries/auth" +import { useInvitationMutation } from "~/queries/invitations" + +const formSchema = z.object({ + code: z.string().min(1), +}) +const absoluteConfettiUrl = new URL(confettiUrl, import.meta.url).href +export const ActivationModalContent = () => { + const form = useForm>({ + resolver: zodResolver(formSchema), + }) + const { setClickOutSideToDismiss } = useCurrentModal() + const invitationMutation = useInvitationMutation({ + onError(error) { + const message = getFetchErrorMessage(error) + form.setError("code", { message }) + }, + }) + + const { t } = useTranslation() + + function onSubmit(values: z.infer) { + invitationMutation.mutate(values.code) + } + + const [showConfetti, setShowConfetti] = useState(false) + + useEffect(() => { + if (invitationMutation.isSuccess) { + setShowConfetti(true) + setClickOutSideToDismiss(true) + auth.getSession().invalidate() + } + }, [invitationMutation.isSuccess, setClickOutSideToDismiss]) + + return ( +
+ + ( + + {t("activation.description")} + + + +
+ +
+
+ )} + /> +
+ {showConfetti && ( + + )} + +
+ + + ) +} diff --git a/apps/renderer/src/modules/activation/NeedActivationToast.tsx b/apps/renderer/src/modules/activation/NeedActivationToast.tsx new file mode 100644 index 000000000..ec9a6b389 --- /dev/null +++ b/apps/renderer/src/modules/activation/NeedActivationToast.tsx @@ -0,0 +1,32 @@ +import { useCallback } from "react" +import { useTranslation } from "react-i18next" + +import { Button } from "~/components/ui/button" +import { stopPropagation } from "~/lib/dom" + +import { useActivationModal } from "." + +export const NeedActivationToast = (props: { dimiss: () => void }) => { + const presentActivationModal = useActivationModal() + + const { t } = useTranslation() + return ( +
+
{t("activation.description")}
+ +
+ +
+
+ ) +} diff --git a/apps/renderer/src/modules/activation/index.tsx b/apps/renderer/src/modules/activation/index.tsx new file mode 100644 index 000000000..2ce3a6f99 --- /dev/null +++ b/apps/renderer/src/modules/activation/index.tsx @@ -0,0 +1,20 @@ +import { useCallback } from "react" +import { useTranslation } from "react-i18next" + +import { useModalStack } from "~/components/ui/modal" + +import { ActivationModalContent } from "./ActivationModalContent" + +export const useActivationModal = () => { + const { present } = useModalStack() + const { t } = useTranslation() + return useCallback( + () => + present({ + title: t("activation.title"), + content: ActivationModalContent, + id: "activation", + }), + [present, t], + ) +} diff --git a/apps/renderer/src/modules/app/EnvironmentIndicator.tsx b/apps/renderer/src/modules/app/EnvironmentIndicator.tsx index 285b36537..5f0df421f 100644 --- a/apps/renderer/src/modules/app/EnvironmentIndicator.tsx +++ b/apps/renderer/src/modules/app/EnvironmentIndicator.tsx @@ -1,18 +1,22 @@ import { env } from "@follow/shared/env" +import { useUserRole } from "~/atoms/user" import { Tooltip, TooltipContent, TooltipPortal, TooltipTrigger } from "~/components/ui/tooltip" -export const EnvironmentIndicator = () => ( - - -
- {import.meta.env.MODE} -
-
- - -
{JSON.stringify({ ...env }, null, 2)}
-
-
-
-) +export const EnvironmentIndicator = () => { + const role = useUserRole() + return ( + + +
+ {role}:{import.meta.env.MODE} +
+
+ + +
{JSON.stringify({ ...env }, null, 2)}
+
+
+
+ ) +} diff --git a/apps/renderer/src/pages/settings/(settings)/invitations.tsx b/apps/renderer/src/pages/settings/(settings)/invitations.tsx index 40f131dae..a661db845 100644 --- a/apps/renderer/src/pages/settings/(settings)/invitations.tsx +++ b/apps/renderer/src/pages/settings/(settings)/invitations.tsx @@ -10,9 +10,7 @@ export const loader = defineSettingPageData({ iconName, name: "titles.invitations", priority, - hideIf(ctx) { - return ctx.role !== UserRole.Trial - }, + hideIf: (ctx) => ctx.role === UserRole.Trial, }) export function Component() { diff --git a/apps/renderer/src/pages/settings/(settings)/list.tsx b/apps/renderer/src/pages/settings/(settings)/list.tsx index 996b214f2..beb610a56 100644 --- a/apps/renderer/src/pages/settings/(settings)/list.tsx +++ b/apps/renderer/src/pages/settings/(settings)/list.tsx @@ -10,9 +10,7 @@ export const loader = defineSettingPageData({ iconName, name: "titles.lists", priority, - hideIf(ctx) { - return ctx.role !== UserRole.Trial - }, + hideIf: (ctx) => ctx.role === UserRole.Trial, }) export function Component() { diff --git a/locales/app/en.json b/locales/app/en.json index a34d7e520..3587f6756 100644 --- a/locales/app/en.json +++ b/locales/app/en.json @@ -3,6 +3,9 @@ "achievement.first_claim_feed": "Feed Owner", "achievement.first_claim_feed_description": "You own your feed on Follow", "achievement.mint_more_power": "Be a hardcore player and earn more ", + "activation.activate": "Activate", + "activation.description": "During the public beta, users who have not used their activation code can only use some of the features.", + "activation.title": "Invitation Code", "ai_daily.header": "AI Daily Report", "ai_daily.no_found": "No AI news found for this period.", "ai_daily.title": "Top News - {{title}}",