feat(plan): show life-time plan name in user profile (#2360)

This commit is contained in:
Huang Xin
2025-10-29 22:36:12 +08:00
committed by GitHub
parent 1d87dcdb7f
commit 9c5bcb13e2
34 changed files with 89 additions and 48 deletions
@@ -659,5 +659,6 @@
"Squiggly": "متعرج",
"Outline": "مخطط",
"Save Current Color": "حفظ اللون الحالي",
"Quick Colors": "ألوان سريعة"
"Quick Colors": "ألوان سريعة",
"Highlighter": "محدد النص"
}
@@ -643,5 +643,6 @@
"Squiggly": "ঢেউখেলানো দাগ",
"Outline": "বাহিরের রেখা",
"Save Current Color": "বর্তমান রঙ সংরক্ষণ করুন",
"Quick Colors": "দ্রুত রংসমূহ"
"Quick Colors": "দ্রুত রংসমূহ",
"Highlighter": "হাইলাইটার"
}
@@ -639,5 +639,6 @@
"Squiggly": "འཁྱོག་ཐིག",
"Outline": "མཐའ་ཐིག",
"Save Current Color": "མིག་སྔའི་ཚོན་གཏན་འཁེལ་བྱེད།",
"Quick Colors": "མྱུར་ཚོན།"
"Quick Colors": "མྱུར་ཚོན།",
"Highlighter": "མདོག་ཚད་ཀྱི་བསྡུར་བ།"
}
@@ -643,5 +643,6 @@
"Squiggly": "Wellig",
"Outline": "Umriss",
"Save Current Color": "Aktuelle Farbe speichern",
"Quick Colors": "Schnellfarben"
"Quick Colors": "Schnellfarben",
"Highlighter": "Textmarker"
}
@@ -643,5 +643,6 @@
"Squiggly": "Κυματιστό",
"Outline": "Περίγραμμα",
"Save Current Color": "Αποθήκευση τρέχουσας χρώματος",
"Quick Colors": "Γρήγορα χρώματα"
"Quick Colors": "Γρήγορα χρώματα",
"Highlighter": "Υπογραμμιστής"
}
@@ -647,5 +647,6 @@
"Squiggly": "Ondulado",
"Outline": "Contorno",
"Save Current Color": "Guardar Color Actual",
"Quick Colors": "Colores Rápidos"
"Quick Colors": "Colores Rápidos",
"Highlighter": "Resaltador"
}
@@ -643,5 +643,6 @@
"Squiggly": "خط‌موج‌دار",
"Outline": "خط‌کشی",
"Save Current Color": "ذخیره رنگ فعلی",
"Quick Colors": "رنگ‌های سریع"
"Quick Colors": "رنگ‌های سریع",
"Highlighter": "ماژیک هایلایت"
}
@@ -647,5 +647,6 @@
"Squiggly": "Ondulé",
"Outline": "Contour",
"Save Current Color": "Enregistrer la Couleur Actuelle",
"Quick Colors": "Couleurs Rapides"
"Quick Colors": "Couleurs Rapides",
"Highlighter": "Surligneur"
}
@@ -643,5 +643,6 @@
"Squiggly": "स्क्विगली",
"Outline": "आउटलाइन",
"Save Current Color": "वर्तमान रंग सहेजें",
"Quick Colors": "त्वरित रंग"
"Quick Colors": "त्वरित रंग",
"Highlighter": "हाइलाइटर"
}
@@ -639,5 +639,6 @@
"Squiggly": "Bergelombang",
"Outline": "Garis Besar",
"Save Current Color": "Simpan Warna Saat Ini",
"Quick Colors": "Warna Cepat"
"Quick Colors": "Warna Cepat",
"Highlighter": "Penyorot"
}
@@ -647,5 +647,6 @@
"Squiggly": "Ondulato",
"Outline": "Contorno",
"Save Current Color": "Salva Colore Corrente",
"Quick Colors": "Colori Veloci"
"Quick Colors": "Colori Veloci",
"Highlighter": "Evidenziatore"
}
@@ -639,5 +639,6 @@
"Squiggly": "波線",
"Outline": "アウトライン",
"Save Current Color": "現在の色を保存",
"Quick Colors": "クイックカラー"
"Quick Colors": "クイックカラー",
"Highlighter": "蛍光ペン"
}
@@ -639,5 +639,6 @@
"Squiggly": "물결선",
"Outline": "윤곽선",
"Save Current Color": "현재 색상 저장",
"Quick Colors": "빠른 색상"
"Quick Colors": "빠른 색상",
"Highlighter": "형광펜"
}
@@ -643,5 +643,6 @@
"Squiggly": "Kronkelig",
"Outline": "Omtrek",
"Save Current Color": "Huidige kleur opslaan",
"Quick Colors": "Snelle kleuren"
"Quick Colors": "Snelle kleuren",
"Highlighter": "Marker"
}
@@ -651,5 +651,6 @@
"Squiggly": "Falista linia",
"Outline": "Obrys",
"Save Current Color": "Zapisz bieżący kolor",
"Quick Colors": "Szybkie kolory"
"Quick Colors": "Szybkie kolory",
"Highlighter": "Zakreślacz"
}
@@ -647,5 +647,6 @@
"Squiggly": "Ondulado",
"Outline": "Contorno",
"Save Current Color": "Salvar cor atual",
"Quick Colors": "Cores rápidas"
"Quick Colors": "Cores rápidas",
"Highlighter": "Marcador"
}
@@ -651,5 +651,6 @@
"Squiggly": "Волнистая линия",
"Outline": "Контур",
"Save Current Color": "Сохранить текущий цвет",
"Quick Colors": "Быстрые цвета"
"Quick Colors": "Быстрые цвета",
"Highlighter": "Маркер"
}
@@ -643,5 +643,6 @@
"Squiggly": "වැලි රේඛාව",
"Outline": "පරිසීමාව",
"Save Current Color": "දැන් ඇති වර්ණය සුරකින්න",
"Quick Colors": "ක්ෂණික වර්ණ"
"Quick Colors": "ක්ෂණික වර්ණ",
"Highlighter": "ඉස්මතු කරන්නා"
}
@@ -643,5 +643,6 @@
"Squiggly": "Vågig linje",
"Outline": "Kontur",
"Save Current Color": "Spara aktuell färg",
"Quick Colors": "Snabba färger"
"Quick Colors": "Snabba färger",
"Highlighter": "Markeringspenna"
}
@@ -643,5 +643,6 @@
"Squiggly": "சுருள் கோடு",
"Outline": "வட்டம்",
"Save Current Color": "தற்போதைய நிறத்தை சேமிக்கவும்",
"Quick Colors": "விரைவு நிறங்கள்"
"Quick Colors": "விரைவு நிறங்கள்",
"Highlighter": "ஹைலைட்டர்"
}
@@ -639,5 +639,6 @@
"Squiggly": "เส้นหยัก",
"Outline": "เส้นขอบ",
"Save Current Color": "บันทึกสีปัจจุบัน",
"Quick Colors": "สีด่วน"
"Quick Colors": "สีด่วน",
"Highlighter": "ปากกาเน้นข้อความ"
}
@@ -643,5 +643,6 @@
"Squiggly": "Dalgalı Çizgi",
"Outline": "Kontur",
"Save Current Color": "Mevcut Rengi Kaydet",
"Quick Colors": "Hızlı Renkler"
"Quick Colors": "Hızlı Renkler",
"Highlighter": "Vurgulayıcı"
}
@@ -651,5 +651,6 @@
"Squiggly": "Хвиляста лінія",
"Outline": "Контур",
"Save Current Color": "Зберегти поточний колір",
"Quick Colors": "Швидкі кольори"
"Quick Colors": "Швидкі кольори",
"Highlighter": "Маркер"
}
@@ -639,5 +639,6 @@
"Squiggly": "Gạch ngoằn ngoèo",
"Outline": "Đường viền",
"Save Current Color": "Lưu màu hiện tại",
"Quick Colors": "Màu nhanh"
"Quick Colors": "Màu nhanh",
"Highlighter": "Bút đánh dấu"
}
@@ -639,5 +639,6 @@
"Squiggly": "波浪线",
"Outline": "轮廓",
"Save Current Color": "保存当前颜色",
"Quick Colors": "快速颜色"
"Quick Colors": "快速颜色",
"Highlighter": "荧光笔"
}
@@ -639,5 +639,6 @@
"Squiggly": "波浪線",
"Outline": "輪廓",
"Save Current Color": "儲存目前顏色",
"Quick Colors": "快速顏色"
"Quick Colors": "快速顏色",
"Highlighter": "螢光筆"
}
+2 -2
View File
@@ -24,7 +24,7 @@ import { start, cancel, onUrl, onInvalidUrl } from '@fabianlars/tauri-plugin-oau
import { openUrl } from '@tauri-apps/plugin-opener';
import { invoke } from '@tauri-apps/api/core';
import { handleAuthCallback } from '@/helpers/auth';
import { getUserPlan } from '@/utils/access';
import { getUserProfilePlan } from '@/utils/access';
import { getAppleIdAuth, Scope } from './utils/appleIdAuth';
import { authWithCustomTab, authWithSafari } from './utils/nativeAuth';
import WindowButtons from '@/components/WindowButtons';
@@ -173,7 +173,7 @@ export default function AuthPage() {
const type = params.get('type');
if (accessToken) {
let next = params.get('next') ?? '/';
if (getUserPlan(accessToken) === 'free') {
if (getUserProfilePlan(accessToken) === 'free') {
next = '/user';
}
handleAuthCallback({ accessToken, refreshToken, type, next, login, navigate: router.push });
@@ -39,7 +39,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
const router = useRouter();
const { envConfig, appService } = useEnv();
const { user } = useAuth();
const { userPlan, quotas } = useQuotaStats(true);
const { userProfilePlan, quotas } = useQuotaStats(true);
const { themeMode, setThemeMode } = useThemeStore();
const { settings, setSettingsDialogOpen } = useSettingsStore();
const [isAutoUpload, setIsAutoUpload] = useState(settings.autoUpload);
@@ -305,7 +305,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
</>
)}
<hr aria-hidden='true' className='border-base-200 my-1' />
{user && userPlan === 'free' && !appService?.isIOSApp && (
{user && userProfilePlan === 'free' && (
<MenuItem label={_('Upgrade to Readest Premium')} onClick={handleUpgrade} />
)}
{isWebAppPlatform() && <MenuItem label={_('Download Readest')} onClick={downloadReadest} />}
+6 -6
View File
@@ -67,7 +67,7 @@ const ProfilePage = () => {
useTheme({ systemUIVisible: false });
const { quotas, userPlan = 'free' } = useQuotaStats();
const { quotas, userProfilePlan = 'free' } = useQuotaStats();
const { handleLogout, handleResetPassword, handleConfirmDelete } = useUserActions();
const { availablePlans } = useAvailablePlans({
@@ -218,8 +218,8 @@ const ProfilePage = () => {
const avatarUrl = user?.user_metadata?.['picture'] || user?.user_metadata?.['avatar_url'];
const userFullName = user?.user_metadata?.['full_name'] || '-';
const userEmail = user?.email || '';
const planDetails =
getPlanDetails(userPlan, availablePlans) || getPlanDetails('free', availablePlans);
const userPlanDetails =
getPlanDetails(userProfilePlan, availablePlans) || getPlanDetails('free', availablePlans);
return (
<div
@@ -258,7 +258,7 @@ const ProfilePage = () => {
avatarUrl={avatarUrl}
userFullName={userFullName}
userEmail={userEmail}
planDetails={planDetails}
planDetails={userPlanDetails}
/>
<UsageStats quotas={quotas} />
@@ -267,14 +267,14 @@ const ProfilePage = () => {
<div className='flex flex-col gap-y-8 sm:px-6'>
<PlansComparison
availablePlans={availablePlans}
userPlan={userPlan}
userPlan={userProfilePlan}
onSubscribe={appService.hasIAP ? handleIAPSubscribe : handleStripeSubscribe}
/>
</div>
<div className='flex flex-col gap-y-8 px-6'>
<AccountActions
userPlan={userPlan}
userPlan={userProfilePlan}
onLogout={handleLogout}
onResetPassword={handleResetPassword}
onConfirmDelete={handleDeleteWithMessage}
@@ -61,7 +61,7 @@ const TTSHighlightStyleEditor: React.FC<TTSHighlightStyleEditorProps> = ({
value={style}
onChange={(e) => onStyleChange(e.target.value as TTSHighlightStyle)}
options={[
{ value: 'highlight', label: _('Highlight') },
{ value: 'highlight', label: _('Highlighter') },
{ value: 'underline', label: _('Underline') },
{ value: 'strikethrough', label: _('Strikethrough') },
{ value: 'squiggly', label: _('Squiggly') },
+4 -5
View File
@@ -1,19 +1,18 @@
import { useEffect, useState } from 'react';
import { useAuth } from '@/context/AuthContext';
import { QuotaType, UserPlan } from '@/types/quota';
import { getStoragePlanData, getTranslationPlanData, getUserPlan } from '@/utils/access';
import { getStoragePlanData, getTranslationPlanData, getUserProfilePlan } from '@/utils/access';
import { useTranslation } from './useTranslation';
export const useQuotaStats = (briefName = false) => {
const _ = useTranslation();
const { token, user } = useAuth();
const [quotas, setQuotas] = useState<QuotaType[]>([]);
const [userPlan, setUserPlan] = useState<UserPlan | undefined>(undefined);
const [userProfilePlan, setUserProfilePlan] = useState<UserPlan | undefined>(undefined);
useEffect(() => {
if (!user || !token) return;
const userPlan = getUserPlan(token);
const storagPlan = getStoragePlanData(token);
const inGB = storagPlan.quota > 1e9;
const storageQuota: QuotaType = {
@@ -35,13 +34,13 @@ export const useQuotaStats = (briefName = false) => {
total: Math.round(translationPlan.quota / 1024),
unit: 'K',
};
setUserPlan(userPlan);
setUserProfilePlan(getUserProfilePlan(token));
setQuotas([storageQuota, translationQuota]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]);
return {
quotas,
userPlan,
userProfilePlan,
};
};
@@ -2,7 +2,11 @@ import crypto from 'crypto';
import { NextApiRequest, NextApiResponse } from 'next';
import { corsAllMethods, runMiddleware } from '@/utils/cors';
import { getCloudflareContext } from '@opennextjs/cloudflare';
import { getDailyTranslationPlanData, getUserPlan, validateUserAndToken } from '@/utils/access';
import {
getDailyTranslationPlanData,
getSubscriptionPlan,
validateUserAndToken,
} from '@/utils/access';
import { ErrorCodes } from '@/services/translators';
import { UsageStatsManager } from '@/utils/usage';
@@ -53,7 +57,7 @@ const updateDailyUsage = async (
if (!userId || !token) return 0;
try {
const userPlan = getUserPlan(token);
const userPlan = getSubscriptionPlan(token);
const newUsage = await UsageStatsManager.trackUsage(
userId,
'translation_chars',
@@ -90,7 +94,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
let deeplApiUrl = deepFreeApiUrl;
let userPlan = 'free';
if (user && token) {
userPlan = getUserPlan(token);
userPlan = getSubscriptionPlan(token);
if (userPlan === 'pro') deeplApiUrl = deeplProApiUrl;
}
const deeplAuthKey =
@@ -2,7 +2,7 @@ import { getAPIBaseUrl } from '@/services/environment';
import { stubTranslation as _ } from '@/utils/misc';
import { ErrorCodes, TranslationProvider } from '../types';
import { UserPlan } from '@/types/quota';
import { getUserPlan } from '@/utils/access';
import { getSubscriptionPlan } from '@/utils/access';
import { normalizeToShortLang } from '@/utils/lang';
import { DEFAULT_DAILY_TRANSLATION_QUOTA } from '@/services/constants';
import { saveDailyUsage } from '../utils';
@@ -29,7 +29,7 @@ export const deeplProvider: TranslationProvider = {
let userPlan: UserPlan = 'free';
if (token) {
userPlan = getUserPlan(token);
userPlan = getSubscriptionPlan(token);
headers['Authorization'] = `Bearer ${token}`;
}
+13 -1
View File
@@ -12,11 +12,23 @@ interface Token {
[key: string]: string | number;
}
export const getUserPlan = (token: string): UserPlan => {
export const getSubscriptionPlan = (token: string): UserPlan => {
const data = jwtDecode<Token>(token) || {};
return data['plan'] || 'free';
};
export const getUserProfilePlan = (token: string): UserPlan => {
const data = jwtDecode<Token>(token) || {};
let plan = data['plan'] || 'free';
if (plan === 'free') {
const purchasedQuota = data['storage_purchased_bytes'] || 0;
if (purchasedQuota > 0) {
plan = 'purchase';
}
}
return plan;
};
export const STORAGE_QUOTA_GRACE_BYTES = 10 * 1024 * 1024; // 10 MB grace
export const getStoragePlanData = (token: string) => {