@@ -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 && (
+
+ )}
+
+
+
+ {/* Header */}
+
+
{tokenPlan.title}
+
+ ${tokenPlan.price}
+ one-time
+
+
+
+ {/* Token amount */}
+
+
+
+
+
+
+ {formatTokens(tokenPlan.tokens)} AI tokens
+
+
+
+
+
+
+
+ {/* 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
})
}