From f3b83e6b873bb017947dabba7bc3f4ba2096db9c Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Wed, 20 Aug 2025 11:27:49 +0800 Subject: [PATCH] feat(desktop): payment for ai (#4309) * feat: payment for ai * update * update * remove referral progress * update * type * update --- .../renderer/src/modules/settings/tabs/ai.tsx | 84 +++ .../src/modules/settings/tabs/plan.tsx | 638 ++++++++++-------- .../src/modules/settings/tabs/referral.tsx | 20 +- .../renderer/src/providers/user-provider.tsx | 8 +- apps/mobile/src/constants/views.tsx | 1 + .../src/modules/settings/SettingsList.tsx | 3 +- .../src/modules/settings/routes/Plan.tsx | 19 +- .../src/modules/settings/routes/Referral.tsx | 21 +- locales/ai/en.json | 5 + packages/internal/constants/src/enums.ts | 2 - .../internal/store/src/modules/entry/store.ts | 1 + .../store/src/modules/subscription/store.ts | 2 + .../internal/store/src/modules/user/store.ts | 3 +- 13 files changed, 485 insertions(+), 322 deletions(-) diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai.tsx index d0abd9693..7e1bae11e 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/tabs/ai.tsx +++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/ai.tsx @@ -4,6 +4,7 @@ import { Input, TextArea } from "@follow/components/ui/input/index.js" import { KbdCombined } from "@follow/components/ui/kbd/Kbd.js" import { KeyValueEditor } from "@follow/components/ui/key-value-editor/index.js" import { Label } from "@follow/components/ui/label/index.jsx" +import { Progress } from "@follow/components/ui/progress/index.jsx" import { Select, SelectContent, @@ -31,6 +32,7 @@ import { } from "~/atoms/settings/ai" import { m } from "~/components/common/Motion" import { useDialog, useModalStack } from "~/components/ui/modal/stacked/hooks" +import { apiFetch } from "~/lib/api-fetch" import { createMCPConnection, deleteMCPConnection, @@ -55,6 +57,11 @@ export const SettingAI = () => {
{ ) } +const useTokenUsage = () => { + return useQuery({ + queryKey: ["aiTokenUsage"], + queryFn: async () => { + // TODO: replace with api client call + return ( + (await apiFetch("/ai/usage")) as { + code: 0 + data: { + total: number + used: number + remaining: number + resetAt: string + } + } + ).data + }, + }) +} + +const TokenUsageSection = () => { + const { t } = useTranslation("ai") + + const tokenUsage = useTokenUsage().data || { + total: 0, + used: 0, + remaining: 0, + resetAt: new Date(), + } + + const usagePercentage = tokenUsage.total === 0 ? 0 : (tokenUsage.used / tokenUsage.total) * 100 + const resetDate = new Date(tokenUsage.resetAt) + + return ( +
+ {t("token_usage.description")} + +
+
+
+

+ {t("token_usage.tokens_used", { + used: tokenUsage.used.toLocaleString(), + total: tokenUsage.total.toLocaleString(), + })} +

+

+ {tokenUsage.remaining.toLocaleString()} {t("token_usage.tokens_remaining")} +

+
+ +
+
{Math.round(usagePercentage)}%
+
+
+ +
+ + +
+ 0 + + {t("token_usage.resets_at")}: {resetDate.toLocaleDateString()} + + {tokenUsage.total.toLocaleString()} +
+
+ +
+
+ Current usage +
+
+
+ ) +} + const PersonalizePromptSetting = () => { const { t } = useTranslation("ai") const aiSettings = useAISettingValue() diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/plan.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/plan.tsx index a154da44a..4ce76059a 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/tabs/plan.tsx +++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/plan.tsx @@ -1,26 +1,22 @@ import { Button } from "@follow/components/ui/button/index.js" -import { Divider } from "@follow/components/ui/divider/Divider.js" import { UserRole, UserRoleName } from "@follow/constants" import { DEEPLINK_SCHEME, IN_ELECTRON } from "@follow/shared" import { env } from "@follow/shared/env.desktop" -import { useRoleEndAt, useUserRole } from "@follow/store/user/hooks" +import { useRoleEndAt, useUserRole, useWhoami } from "@follow/store/user/hooks" import { cn } from "@follow/utils/utils" -import { useMutation } from "@tanstack/react-query" -import dayjs from "dayjs" +import { useMutation, useQuery } from "@tanstack/react-query" +import { useState } from "react" import { Trans } from "react-i18next" +import { toast } from "sonner" -import { useServerConfigs } from "~/atoms/server-configs" import { subscription } from "~/lib/auth" -import { useReferralInfo } from "~/queries/referral" - -import { useSetSettingTab } from "../modal/context" // Plan configuration types interface Plan { id: string title: string - price: string - period: string + monthlyPrice: number + yearlyPrice: number features: string[] isPopular?: boolean role: UserRole @@ -33,8 +29,7 @@ const PLAN_TIER_MAP: Record = { [UserRole.Admin]: 4, // Admin has highest tier [UserRole.Free]: 1, [UserRole.Trial]: 1, // Same as Free (deprecated) - [UserRole.PreProTrial]: 2, // Same tier as PrePro - [UserRole.PrePro]: 2, + [UserRole.PreProTrial]: 2, [UserRole.Pro]: 3, } @@ -43,41 +38,72 @@ const PLAN_CONFIGS: Plan[] = [ { id: "free", title: UserRoleName[UserRole.Free], - price: "$0", - period: "", + monthlyPrice: 0, + yearlyPrice: 0, features: ["50 feeds", "10 lists"], isPopular: false, role: UserRole.Free, tier: PLAN_TIER_MAP[UserRole.Free], }, { - id: "pro-preview", - title: UserRoleName[UserRole.PrePro], - price: "$1 or 3 invitations", - period: "", - features: ["1000 feeds and lists", "10 inboxes", "10 actions", "100 webhooks"], - isPopular: false, - role: UserRole.PrePro, - tier: PLAN_TIER_MAP[UserRole.PrePro], - }, - { - id: "pro", + id: "folo pro", title: UserRoleName[UserRole.Pro], - price: "Coming soon", - period: "", - features: [`Everything in ${UserRoleName[UserRole.PrePro]}`, "Advanced AI features"], - isPopular: false, + monthlyPrice: 20, + yearlyPrice: 180, + features: [ + "1000 feeds and lists", + "10 inboxes", + "10 actions", + "100 webhooks", + "Advanced AI features", + ], + isPopular: true, role: UserRole.Pro, - isComingSoon: true, tier: PLAN_TIER_MAP[UserRole.Pro], }, ] -const useUpgradePlan = () => { +// AI Token configuration types +interface AITokenPlan { + id: string + title: string + price: number + tokens: number + isPopular?: boolean +} +const AI_TOKEN_IN_PRO_PLAN = 15_000_000 +const AI_TOKEN_IN_PRO_PLAN_PER_DOLLAR = AI_TOKEN_IN_PRO_PLAN / 10 +// AI Token configurations +const AI_TOKEN_CONFIGS: AITokenPlan[] = [ + { + id: "folo ai token ($5)", + title: "Folo AI Token ($5)", + price: 5, + tokens: AI_TOKEN_IN_PRO_PLAN_PER_DOLLAR * 5, + isPopular: false, + }, + { + id: "folo ai token ($10)", + title: "Folo AI Token ($10)", + price: 10, + tokens: AI_TOKEN_IN_PRO_PLAN_PER_DOLLAR * 10, + isPopular: true, + }, + { + id: "folo ai token ($20)", + title: "Folo AI Token ($20)", + price: 20, + tokens: AI_TOKEN_IN_PRO_PLAN_PER_DOLLAR * 20, + isPopular: false, + }, +] + +const useUpgradePlan = ({ plan, annual }: { plan: string; annual: boolean }) => { return useMutation({ mutationFn: async () => { const res = await subscription.upgrade({ - plan: "folo pro preview", + plan, + annual, successUrl: IN_ELECTRON ? `${DEEPLINK_SCHEME}refresh` : env.VITE_WEB_URL, cancelUrl: env.VITE_WEB_URL, disableRedirect: IN_ELECTRON, @@ -89,43 +115,115 @@ const useUpgradePlan = () => { }) } +const usePurchaseAITokens = ({ tokenPlan }: { tokenPlan: string }) => { + return useMutation({ + mutationFn: async () => { + // TODO: Implement AI token purchase API call + const res = await subscription.upgrade({ + plan: tokenPlan, + annual: false, + successUrl: IN_ELECTRON ? `${DEEPLINK_SCHEME}refresh` : env.VITE_WEB_URL, + cancelUrl: env.VITE_WEB_URL, + disableRedirect: IN_ELECTRON, + }) + if (IN_ELECTRON && res.data?.url) { + window.open(res.data.url, "_blank") + } + }, + }) +} + +const useActiveSubscription = () => { + const userId = useWhoami()?.id + return useQuery({ + queryKey: ["activeSubscription"], + queryFn: async () => { + const { data } = await subscription.list() + return data + }, + enabled: !!userId, + }) +} + +const useCancelPlan = () => { + const activeSubscription = useActiveSubscription() + const latestSubscription = activeSubscription.data?.at(-1) + const subscriptionId = latestSubscription?.id + const cancelAtPeriodEnd = latestSubscription?.cancelAtPeriodEnd + + const cancelMutation = useMutation({ + mutationFn: async () => { + if (!subscriptionId) { + toast.error("No active subscription found.") + return + } + + await subscription.cancel({ + subscriptionId, + returnUrl: IN_ELECTRON ? `${DEEPLINK_SCHEME}refresh` : env.VITE_WEB_URL, + fetchOptions: { + onError(context) { + toast.error(context.error.message) + }, + }, + }) + }, + }) + + if (cancelAtPeriodEnd) { + return null + } else { + return cancelMutation + } +} + export function SettingPlan() { - const serverConfigs = useServerConfigs() - const requiredInvitationsAmount = serverConfigs?.REFERRAL_REQUIRED_INVITATIONS || 3 - const skipPrice = serverConfigs?.REFERRAL_PRO_PREVIEW_STRIPE_PRICE_IN_DOLLAR || 1 - const ruleLink = serverConfigs?.REFERRAL_RULE_LINK - const { data: referralInfo } = useReferralInfo() - const validInvitationsAmount = referralInfo?.invitations.filter((i) => i.usedAt).length || 0 const role = useUserRole() const roleEndDate = useRoleEndAt() + const [billingPeriod, setBillingPeriod] = useState<"monthly" | "yearly">("yearly") + const daysLeft = roleEndDate ? Math.ceil((roleEndDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)) : null - const upgradePlanMutation = useUpgradePlan() - return (
{/* Description Section */}

- - ), - }} - /> +

+ {/* Billing Period Toggle */} +
+
+ + +
+
+ {/* Plans Grid */}
@@ -133,209 +231,52 @@ export function SettingPlan() { ))}
- - {/* Current Status Card */} - { - upgradePlanMutation.mutate() - }} - isLoading={upgradePlanMutation.isPending} - /> + {/* AI Tokens Section */} +
+
+

AI Tokens

+

+ Purchase additional AI tokens for enhanced content processing and analysis. +

+
+ +
+
+ {AI_TOKEN_CONFIGS.map((tokenPlan) => ( + + ))} +
+
+
) } -// Reusable StatusCard Component -interface StatusCardProps { - role: UserRole - roleEndDate: Date | null - daysLeft: number | null - validInvitationsAmount: number - requiredInvitationsAmount: number - skipPrice: number - onUpgrade: () => void - isLoading?: boolean -} - -const StatusCard = ({ - role, - roleEndDate, - daysLeft, - validInvitationsAmount, - requiredInvitationsAmount, - skipPrice, - onUpgrade, - isLoading, -}: StatusCardProps) => { - const getStatusInfo = () => { - if (role === UserRole.PrePro) { - return { - title: "You have an active Pro Preview plan", - icon: "i-mgc-check-cute-fi", - iconBg: "bg-green", - } - } - if (role === UserRole.PreProTrial) { - return { - title: `Pro Preview trial expires ${dayjs(roleEndDate).format("MMMM D, YYYY")} (${daysLeft} days left)`, - icon: "i-mgc-time-cute-re", - iconBg: "bg-accent", - } - } - return { - title: "Start your journey with our referral program", - icon: "i-mgc-time-cute-re", - iconBg: "bg-accent", - } - } - - const statusInfo = getStatusInfo() - - return ( -
-
-
- - - - - {role !== UserRole.PrePro && ( - - )} -
-
- ) -} - -// Reusable StatusHeader Component -interface StatusHeaderProps { - title: string - description: string - icon: string - iconBg: string -} - -const StatusHeader = ({ title, description, icon, iconBg }: StatusHeaderProps) => ( -
-
- -
-
-

{title}

-

{description}

-
-
-) - -// Reusable ReferralProgress Component -interface ReferralProgressProps { - validInvitationsAmount: number - requiredInvitationsAmount: number -} - -const ReferralProgress = ({ - validInvitationsAmount, - requiredInvitationsAmount, -}: ReferralProgressProps) => ( -
-
- Referral Progress - - {validInvitationsAmount} / {requiredInvitationsAmount} - -
- -
-
-
-
- {validInvitationsAmount >= requiredInvitationsAmount && } -
-
-) - -// Reusable CompletionBadge Component -const CompletionBadge = () => ( -
- -
-) - -// Reusable UpgradeSection Component -interface UpgradeSectionProps { - skipPrice: number - onUpgrade: () => void - isLoading?: boolean -} - -const UpgradeSection = ({ skipPrice, onUpgrade, isLoading }: UpgradeSectionProps) => { - const setTab = useSetSettingTab() - - return ( -
-
- - or - -
-
- ) -} - // Reusable PlanCard Component interface PlanCardProps { plan: Plan + billingPeriod: "monthly" | "yearly" currentUserRole: UserRole | null isCurrentPlan: boolean daysLeft: number | null } -const PlanCard = ({ plan, currentUserRole, isCurrentPlan, daysLeft }: PlanCardProps) => { +const PlanCard = ({ + plan, + billingPeriod, + currentUserRole, + isCurrentPlan, + daysLeft, +}: PlanCardProps) => { const getPlanActionType = (): "current" | "upgrade" | "coming-soon" | "in-trial" | null => { if (plan.isComingSoon) return "coming-soon" @@ -347,18 +288,36 @@ const PlanCard = ({ plan, currentUserRole, isCurrentPlan, daysLeft }: PlanCardPr const targetTier = plan.tier if (currentTier === targetTier) { - if (currentUserRole === UserRole.PreProTrial) { - return "in-trial" - } else { - return "current" - } + // if (currentUserRole === UserRole.PreProTrial) { + // return "in-trial" + // } else { + // return "current" + // } + return "current" } if (targetTier > currentTier) return "upgrade" return null } const actionType = getPlanActionType() - const upgradePlanMutation = useUpgradePlan() + const upgradePlanMutation = useUpgradePlan({ + plan: "folo pro", + annual: billingPeriod === "yearly", + }) + const cancelPlanMutation = useCancelPlan() + + // Calculate price and period based on billing period + const price = billingPeriod === "yearly" ? plan.yearlyPrice : plan.monthlyPrice + const formattedPrice = price === 0 ? "$0" : `$${price}` + const period = plan.role === UserRole.Free ? "" : billingPeriod === "yearly" ? "year" : "month" + + // Calculate savings for yearly plan + const yearlyTotalPrice = plan.yearlyPrice + const monthlyTotalPrice = plan.monthlyPrice * 12 + const savingsPercentage = + plan.monthlyPrice > 0 && plan.yearlyPrice > 0 + ? Math.round(((monthlyTotalPrice - yearlyTotalPrice) / monthlyTotalPrice) * 100) + : 0 return (
- +
@@ -385,18 +352,21 @@ const PlanCard = ({ plan, currentUserRole, isCurrentPlan, daysLeft }: PlanCardPr isPopular={plan.isPopular || false} actionType={actionType} daysLeft={daysLeft} - isLoading={upgradePlanMutation.isPending} - onSelect={() => { - if ( - !plan.isComingSoon && - !isCurrentPlan && // Handle plan selection logic - plan.role === UserRole.PrePro - ) { - // Trigger upgrade to Pro Preview - upgradePlanMutation.mutate() - } - // Add other plan selection logic as needed - }} + isLoading={upgradePlanMutation.isPending || cancelPlanMutation?.isPending} + onSelect={ + !plan.isComingSoon && !isCurrentPlan + ? () => { + upgradePlanMutation.mutate() + } + : undefined + } + onCancel={ + isCurrentPlan && plan.role !== UserRole.Free && cancelPlanMutation + ? () => { + cancelPlanMutation.mutate() + } + : undefined + } />
@@ -419,20 +389,54 @@ const PlanBadges = ({ isPopular }: { isPopular: boolean }) => ( ) -const PlanHeader = ({ title, price, period }: { title: string; price: string; period: string }) => ( +const PlanHeader = ({ + title, + price, + period, + billingPeriod, + monthlyPrice, + yearlyPrice, + savingsPercentage, +}: { + title: string + price: string + period: string + billingPeriod: "monthly" | "yearly" + monthlyPrice: number + yearlyPrice: number + savingsPercentage: number +}) => (

{title}

- {price} + {price} {period && /{period}}
+
+ {billingPeriod === "yearly" && + monthlyPrice > 0 && + yearlyPrice > 0 && + savingsPercentage > 0 && ( +
+ + ${(monthlyPrice * 12).toFixed(0)}/year + + Save {savingsPercentage}% +
+ )} + {billingPeriod === "yearly" && monthlyPrice > 0 && yearlyPrice > 0 && ( +
+ ${(yearlyPrice / 12).toFixed(1)}/month billed annually +
+ )} +
) const PlanFeatures = ({ features }: { features: string[] }) => (
- {features.map((feature, index) => ( -
+ {features.map((feature) => ( +
@@ -446,12 +450,14 @@ const PlanAction = ({ isPopular, actionType, onSelect, + onCancel, isLoading, daysLeft, }: { isPopular: boolean actionType: "current" | "upgrade" | "coming-soon" | "in-trial" | null onSelect?: () => void + onCancel?: () => void isLoading?: boolean daysLeft: number | null }) => { @@ -467,7 +473,7 @@ const PlanAction = ({ } case "current": { return { - text: "Current Plan", + text: typeof daysLeft === "number" ? `${daysLeft} days left` : "Current Plan", variant: "outline" as const, className: "w-full h-9 @md:h-10 text-xs @md:text-sm text-text-secondary", disabled: true, @@ -504,7 +510,7 @@ const PlanAction = ({ } return ( -
+
+ + {actionType === "current" && typeof daysLeft === "number" && onCancel && ( + + )} +
+ ) +} + +// AI Token Card Component +interface AITokenCardProps { + tokenPlan: AITokenPlan +} + +const AITokenCard = ({ tokenPlan }: AITokenCardProps) => { + const purchaseTokensMutation = usePurchaseAITokens({ + tokenPlan: tokenPlan.id, + }) + + // Format tokens display + const formatTokens = (tokens: number): string => { + if (tokens >= 1000000) { + return `${(tokens / 1000000).toFixed(1)}M` + } + if (tokens >= 1000) { + return `${(tokens / 1000).toFixed(0)}K` + } + return tokens.toString() + } + + return ( +
+ {/* Popular badge */} + {tokenPlan.isPopular && ( +
+
+ Most Popular +
+
+ )} + +
+
+ {/* Header */} +
+

{tokenPlan.title}

+
+ ${tokenPlan.price} + one-time +
+
+ + {/* Token amount */} +
+
+
+ +
+ + {formatTokens(tokenPlan.tokens)} AI tokens + +
+
+
+ +
+ Never expires +
+
+
+
+ + {/* Purchase button */} + +
+ + {/* Subtle gradient line at bottom */} +
) } diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/referral.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/referral.tsx index 4bff16c1e..e7f955d71 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/tabs/referral.tsx +++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/referral.tsx @@ -1,5 +1,4 @@ import { Divider } from "@follow/components/ui/divider/Divider.js" -import { Progress } from "@follow/components/ui/progress/index.js" import { Table, TableBody, @@ -14,9 +13,8 @@ import { TooltipPortal, TooltipTrigger, } from "@follow/components/ui/tooltip/index.js" -import { UserRole, UserRoleName } from "@follow/constants" import { env } from "@follow/shared/env.desktop" -import { useUserRole, useWhoami } from "@follow/store/user/hooks" +import { useWhoami } from "@follow/store/user/hooks" import dayjs from "dayjs" import { Trans, useTranslation } from "react-i18next" @@ -29,12 +27,9 @@ import { useReferralInfo } from "~/queries/referral" export function SettingReferral() { const { t } = useTranslation("settings") const serverConfigs = useServerConfigs() - const requiredInvitationsAmount = serverConfigs?.REFERRAL_REQUIRED_INVITATIONS || 3 const ruleLink = serverConfigs?.REFERRAL_RULE_LINK const { data: referralInfo } = useReferralInfo() - const validInvitationsAmount = referralInfo?.invitations.filter((i) => i.usedAt).length || 0 const user = useWhoami() - const role = useUserRole() const referralLink = `Here's the link to register for Folo, the reader I mentioned! Use it to enjoy a 45-day trial of Pro features:\n\n${env.VITE_WEB_URL}/register?referral=${user?.handle || user?.id}` const presentUserProfile = usePresentUserProfileModal("drawer") return ( @@ -59,19 +54,6 @@ export function SettingReferral() {
{referralLink}
- {role !== UserRole.PrePro && ( -
-

- Referral Progress for the {UserRoleName[UserRole.PrePro]}: -

-
- - - {validInvitationsAmount} / {requiredInvitationsAmount} - -
-
- )} {!!referralInfo?.invitations.length && ( <> diff --git a/apps/desktop/layer/renderer/src/providers/user-provider.tsx b/apps/desktop/layer/renderer/src/providers/user-provider.tsx index de51095bd..593348451 100644 --- a/apps/desktop/layer/renderer/src/providers/user-provider.tsx +++ b/apps/desktop/layer/renderer/src/providers/user-provider.tsx @@ -34,11 +34,13 @@ export const UserProvider = () => { const isToastDismissed = localStorage.getItem(itemKey) - if (session.role && session.role !== UserRole.PrePro && !isToastDismissed) { + const sessionRole = session.role as string + + if (sessionRole && sessionRole !== UserRole.Pro && !isToastDismissed) { const message = - session.role === UserRole.Free || session.role === UserRole.Trial + sessionRole === UserRole.Free || sessionRole === UserRole.Trial ? `You are currently on the ${UserRoleName[UserRole.Free]} plan. Some features may be limited.` - : session.role === UserRole.PreProTrial + : sessionRole === UserRole.PreProTrial ? `You are currently on the ${UserRoleName[UserRole.PreProTrial]} plan.${roleEndDate ? ` It will end on ${roleEndDate.toLocaleDateString()}.` : ""}` : "" if (!message) { diff --git a/apps/mobile/src/constants/views.tsx b/apps/mobile/src/constants/views.tsx index 412eb8cb6..1ae73488b 100644 --- a/apps/mobile/src/constants/views.tsx +++ b/apps/mobile/src/constants/views.tsx @@ -11,6 +11,7 @@ import { VideoCuteFiIcon } from "../icons/video_cute_fi" interface ViewDefinitionExtended { icon: React.FC<{ color?: string; height?: number; width?: number }> } +// @ts-expect-error FIXME const extendMap: Record = { [FeedViewType.Articles]: { icon: PaperCuteFiIcon, diff --git a/apps/mobile/src/modules/settings/SettingsList.tsx b/apps/mobile/src/modules/settings/SettingsList.tsx index 57ccd73d8..517f2c6de 100644 --- a/apps/mobile/src/modules/settings/SettingsList.tsx +++ b/apps/mobile/src/modules/settings/SettingsList.tsx @@ -125,7 +125,8 @@ const ReferralGroupNavigationLinks: GroupNavigationLink[] = [ }, iconBackgroundColor: accentColor, anonymous: false, - hideIf: (serverConfigs) => !serverConfigs?.REFERRAL_ENABLED, + // TODO: support pay on mobile + hideIf: () => true, }, { label: "titles.referral.short", diff --git a/apps/mobile/src/modules/settings/routes/Plan.tsx b/apps/mobile/src/modules/settings/routes/Plan.tsx index 8465bdb64..74480547e 100644 --- a/apps/mobile/src/modules/settings/routes/Plan.tsx +++ b/apps/mobile/src/modules/settings/routes/Plan.tsx @@ -54,8 +54,6 @@ const PLAN_TIER_MAP: Record = { [UserRole.Trial]: 1, // Same as Free (deprecated) [UserRole.PreProTrial]: 2, - // Same tier as PrePro - [UserRole.PrePro]: 2, [UserRole.Pro]: 3, } @@ -73,20 +71,20 @@ const PLAN_CONFIGS: Plan[] = [ }, { id: "pro-preview", - title: UserRoleName[UserRole.PrePro], + title: UserRoleName[UserRole.Pro], price: "$1 or 3 invitations", period: "", features: ["1000 feeds and lists", "10 inboxes", "10 actions", "100 webhooks"], isPopular: false, - role: UserRole.PrePro, - tier: PLAN_TIER_MAP[UserRole.PrePro], + role: UserRole.Pro, + tier: PLAN_TIER_MAP[UserRole.Pro], }, { id: "pro", title: UserRoleName[UserRole.Pro], price: "Coming soon", period: "", - features: [`Everything in ${UserRoleName[UserRole.PrePro]}`, "Advanced AI features"], + features: [`Everything in ${UserRoleName[UserRole.Pro]}`, "Advanced AI features"], isPopular: false, role: UserRole.Pro, isComingSoon: true, @@ -215,8 +213,7 @@ export const PlanScreen: NavigationControllerView = () => { onUpgrade={isProPreview ? () => upgradePlanMutation.mutate() : undefined} disabled={isProPreview && upgradePlanMutation.isPending} isCurrentPlan={ - role === plan.role || - (plan.role === UserRole.PrePro && role === UserRole.PreProTrial) + role === plan.role || (plan.role === UserRole.Pro && role === UserRole.PreProTrial) } /> ) @@ -231,8 +228,8 @@ export const PlanScreen: NavigationControllerView = () => { Current Status - {role === UserRole.PrePro - ? "You have an active Pro Preview plan" + {role === UserRole.Pro + ? "You have an active Pro plan" : role === UserRole.PreProTrial ? `Pro Preview trial expires ${dayjs(roleEndDate).format("MMMM D, YYYY")} (${daysLeft} days left)` : "Start your journey with our referral program"} @@ -255,7 +252,7 @@ export const PlanScreen: NavigationControllerView = () => { /> - {role !== UserRole.PrePro && ( + {role !== UserRole.Pro && ( { const { t } = useTranslation("settings") const serverConfigs = useServerConfigs() const ruleLink = serverConfigs?.REFERRAL_RULE_LINK - const requiredInvitationsAmount = serverConfigs?.REFERRAL_REQUIRED_INVITATIONS || 3 const { data: referralInfo, isLoading } = useReferralInfoQuery() const invitations = referralInfo?.invitations - const validInvitationsAmount = referralInfo?.invitations.filter((i) => i.usedAt).length || 0 const user = useWhoami() const referralLink = `${env.WEB_URL}/register?referral=${user?.handle || user?.id}` const secondaryLabelColor = useColor("secondaryLabel") - const progress = (validInvitationsAmount / requiredInvitationsAmount) * 100 + return ( { - - - - - - - - - {isLoading && } diff --git a/locales/ai/en.json b/locales/ai/en.json index a466be52c..0888da7f4 100644 --- a/locales/ai/en.json +++ b/locales/ai/en.json @@ -87,6 +87,11 @@ "shortcuts.updated": "Shortcut updated successfully", "shortcuts.validation.required": "Name and prompt are required", "summary_not_available": "Summary not available", + "token_usage.description": "Monitor your AI token consumption and limits", + "token_usage.resets_at": "Resets at", + "token_usage.title": "Token Usage", + "token_usage.tokens_remaining": "tokens remaining", + "token_usage.tokens_used": "{{used}} / {{total}} tokens used", "welcome_description": "I'm here to help you with your reading needs. How can I assist you today?", "welcome_description_contextual": "Let's discuss this entry together" } diff --git a/packages/internal/constants/src/enums.ts b/packages/internal/constants/src/enums.ts index f3159b55c..2cd116221 100644 --- a/packages/internal/constants/src/enums.ts +++ b/packages/internal/constants/src/enums.ts @@ -7,7 +7,6 @@ export enum Routes { export enum UserRole { Admin = "admin", PreProTrial = "pre_pro_trial", - PrePro = "pre_pro", Free = "free", /** * @deprecated @@ -21,7 +20,6 @@ export enum UserRole { export const UserRoleName: Record = { [UserRole.Admin]: "Admin", [UserRole.PreProTrial]: "Pro Preview Trial", - [UserRole.PrePro]: "Pro Preview", [UserRole.Free]: "Free", /** * @deprecated diff --git a/packages/internal/store/src/modules/entry/store.ts b/packages/internal/store/src/modules/entry/store.ts index d81705ebc..c2a2af2b3 100644 --- a/packages/internal/store/src/modules/entry/store.ts +++ b/packages/internal/store/src/modules/entry/store.ts @@ -46,6 +46,7 @@ interface EntryState { const defaultState: EntryState = { data: {}, entryIdByView: { + [FeedViewType.All]: new Set(), [FeedViewType.Articles]: new Set(), [FeedViewType.Audios]: new Set(), [FeedViewType.Notifications]: new Set(), diff --git a/packages/internal/store/src/modules/subscription/store.ts b/packages/internal/store/src/modules/subscription/store.ts index 4b938eff6..23f727eec 100644 --- a/packages/internal/store/src/modules/subscription/store.ts +++ b/packages/internal/store/src/modules/subscription/store.ts @@ -48,6 +48,7 @@ export interface SubscriptionState { } const emptyDataSetByView: Record> = { + [FeedViewType.All]: new Set(), [FeedViewType.Articles]: new Set(), [FeedViewType.Audios]: new Set(), [FeedViewType.Notifications]: new Set(), @@ -56,6 +57,7 @@ const emptyDataSetByView: Record> = { [FeedViewType.Videos]: new Set(), } const emptyCategoryOpenStateByView: Record> = { + [FeedViewType.All]: {}, [FeedViewType.Articles]: {}, [FeedViewType.Audios]: {}, [FeedViewType.Notifications]: {}, diff --git a/packages/internal/store/src/modules/user/store.ts b/packages/internal/store/src/modules/user/store.ts index 1c812d9e3..4450fc011 100644 --- a/packages/internal/store/src/modules/user/store.ts +++ b/packages/internal/store/src/modules/user/store.ts @@ -73,6 +73,7 @@ class UserSyncService { const user = honoMorph.toUser(res.user, true) immerSet((state) => { state.whoami = { ...user, emailVerified: res.user.emailVerified } + // @ts-expect-error state.role = res.role if (res.roleEndAt) { state.roleEndAt = new Date(res.roleEndAt) @@ -181,7 +182,7 @@ class UserSyncService { const res = await apiClient().invitations.use.$post({ json: { code } }) if (res.code === 0) { immerSet((state) => { - state.role = UserRole.PrePro + state.role = UserRole.Pro }) }