From 5d61a9808f993563a88e1c70dbd94be7233512a0 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Mon, 30 Jun 2025 16:04:57 +0800 Subject: [PATCH] feat: add subscription management for tauri platforms (#1499) --- apps/readest-app/package.json | 3 +- .../public/locales/ar/translation.json | 8 +- .../public/locales/de/translation.json | 8 +- .../public/locales/el/translation.json | 8 +- .../public/locales/es/translation.json | 8 +- .../public/locales/fr/translation.json | 8 +- .../public/locales/hi/translation.json | 8 +- .../public/locales/id/translation.json | 8 +- .../public/locales/it/translation.json | 8 +- .../public/locales/ja/translation.json | 8 +- .../public/locales/ko/translation.json | 8 +- .../public/locales/nl/translation.json | 8 +- .../public/locales/pl/translation.json | 8 +- .../public/locales/pt/translation.json | 8 +- .../public/locales/ru/translation.json | 8 +- .../public/locales/tr/translation.json | 8 +- .../public/locales/uk/translation.json | 8 +- .../public/locales/vi/translation.json | 8 +- .../public/locales/zh-CN/translation.json | 8 +- .../public/locales/zh-TW/translation.json | 8 +- .../src/app/api/stripe/checkout/route.ts | 16 ++- .../src/app/user/components/Checkout.tsx | 53 +++++++ .../app/user/components/PlansComparison.tsx | 7 +- apps/readest-app/src/app/user/page.tsx | 135 +++++++++++++----- apps/readest-app/src/app/user/utils/plan.ts | 6 +- .../src/components/AboutWindow.tsx | 4 +- apps/readest-app/src/components/Providers.tsx | 8 +- .../src/components/UpdaterWindow.tsx | 8 +- apps/readest-app/src/context/PHContext.tsx | 8 +- apps/readest-app/src/helpers/updater.ts | 4 +- apps/readest-app/src/hooks/useQuotaStats.ts | 4 +- apps/readest-app/src/libs/sync.ts | 4 +- apps/readest-app/src/utils/fetch.ts | 2 +- apps/readest-app/src/utils/version.ts | 5 + pnpm-lock.yaml | 67 ++++++--- 35 files changed, 326 insertions(+), 160 deletions(-) create mode 100644 apps/readest-app/src/app/user/components/Checkout.tsx create mode 100644 apps/readest-app/src/utils/version.ts diff --git a/apps/readest-app/package.json b/apps/readest-app/package.json index 569a1760..2396b5e3 100644 --- a/apps/readest-app/package.json +++ b/apps/readest-app/package.json @@ -43,10 +43,11 @@ "@ducanh2912/next-pwa": "^10.2.9", "@fabianlars/tauri-plugin-oauth": "2", "@opennextjs/cloudflare": "^1.3.1", + "@stripe/react-stripe-js": "^3.7.0", "@stripe/stripe-js": "^7.4.0", "@supabase/auth-ui-react": "^0.4.7", "@supabase/auth-ui-shared": "^0.1.8", - "@supabase/supabase-js": "^2.49.10", + "@supabase/supabase-js": "^2.50.2", "@tauri-apps/api": "2.6.0", "@tauri-apps/plugin-cli": "^2.3.0", "@tauri-apps/plugin-deep-link": "^2.3.0", diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index 9794e3ee..4c1ba794 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -126,8 +126,6 @@ "Failed to download book: {{title}}": "فشل تنزيل الكتاب: {{title}}", "Upload Book": "تحميل الكتاب", "Auto Upload Books to Cloud": "تحميل الكتب تلقائيًا إلى السحابة", - "{{percentage}}% of Cloud Storage Used.": "تم استخدام {{percentage}}% من مساحة التخزين السحابية.", - "Storage": "التخزين", "Book deleted: {{title}}": "تم حذف الكتاب: {{title}}", "Failed to delete book: {{title}}": "فشل حذف الكتاب: {{title}}", "Check Updates on Start": "التحقق من التحديثات عند البدء", @@ -406,7 +404,6 @@ "Unlimited Text-to-Speech Hours": "ساعات تحويل النص إلى كلام غير محدودة", "DeepL Free Access (10K characters/day)": "وصول مجاني إلى DeepL (10 آلاف حرف يوميًا)", "AI Translations (per day)": "ترجمات بالذكاء الاصطناعي (يوميًا)", - "Cloud Storage": "تخزين سحابي", "Plus Plan": "خطة Plus", "Includes All Free Plan Benefits": "تتضمن جميع مزايا الخطة المجانية", "Basic AI Translations": "ترجمات أساسية بالذكاء الاصطناعي", @@ -415,5 +412,8 @@ "Pro Plan": "خطة Pro", "Includes All Plus Plan Benefits": "تتضمن جميع مزايا خطة Plus", "More AI Translations": "المزيد من الترجمات بالذكاء الاصطناعي", - "DeepL Pro Access (200K characters/day)": "وصول احترافي إلى DeepL (200 ألف حرف يوميًا)" + "DeepL Pro Access (200K characters/day)": "وصول احترافي إلى DeepL (200 ألف حرف يوميًا)", + "Complete Your Subscription": "أكمل اشتراكك", + "{{percentage}}% of Cloud Sync Space Used.": "تم استخدام {{percentage}}% من مساحة المزامنة السحابية.", + "Cloud Sync Storage": "مساحة المزامنة السحابية" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index eb45e4cd..dda78bb0 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -126,8 +126,6 @@ "Failed to download book: {{title}}": "Fehler beim Herunterladen des Buches: {{title}}", "Upload Book": "Buch hochladen", "Auto Upload Books to Cloud": "Bücher automatisch hochladen", - "{{percentage}}% of Cloud Storage Used.": "{{percentage}}% des Cloud-Speichers verwendet.", - "Storage": "Speicher", "Book deleted: {{title}}": "Buch gelöscht: {{title}}", "Failed to delete book: {{title}}": "Fehler beim Löschen des Buches: {{title}}", "Check Updates on Start": "Aktualisierungen beim Start prüfen", @@ -390,7 +388,6 @@ "Unlimited Text-to-Speech Hours": "Unbegrenzte Text-to-Speech-Stunden", "DeepL Free Access (10K characters/day)": "Kostenfreier DeepL-Zugang (10.000 Zeichen/Tag)", "AI Translations (per day)": "KI-Übersetzungen (pro Tag)", - "Cloud Storage": "Cloud-Speicher", "Plus Plan": "Plus-Tarif", "Includes All Free Plan Benefits": "Enthält alle Vorteile des Gratis-Tarifs", "Basic AI Translations": "Basis-KI-Übersetzungen", @@ -399,5 +396,8 @@ "Pro Plan": "Pro-Tarif", "Includes All Plus Plan Benefits": "Enthält alle Vorteile des Plus-Tarifs", "More AI Translations": "Mehr KI-Übersetzungen", - "DeepL Pro Access (200K characters/day)": "DeepL Pro-Zugang (200.000 Zeichen/Tag)" + "DeepL Pro Access (200K characters/day)": "DeepL Pro-Zugang (200.000 Zeichen/Tag)", + "Complete Your Subscription": "Abschließen Sie Ihr Abonnement", + "{{percentage}}% of Cloud Sync Space Used.": "{{percentage}}% des Cloud-Synchronisierungsspeichers verwendet.", + "Cloud Sync Storage": "Cloud-Speicher" } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index bbfce0f1..e038e8a4 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -126,8 +126,6 @@ "Failed to download book: {{title}}": "Αποτυχία λήψης βιβλίου: {{title}}", "Upload Book": "Ανέβασμα βιβλίου", "Auto Upload Books to Cloud": "Αυτόματη αποθήκευση στο cloud", - "{{percentage}}% of Cloud Storage Used.": "{{percentage}}% της αποθήκευσης στο cloud χρησιμοποιήθηκε.", - "Storage": "Αποθήκευση", "Book deleted: {{title}}": "Το βιβλίο με τίτλο {{title}} διαγράφηκε", "Failed to delete book: {{title}}": "Αποτυχία διαγραφής βιβλίου: {{title}}", "Check Updates on Start": "Αυτόματος έλεγχος ενημερώσεων", @@ -390,7 +388,6 @@ "Unlimited Text-to-Speech Hours": "Απεριόριστες ώρες μετατροπής κειμένου σε ομιλία", "DeepL Free Access (10K characters/day)": "Δωρεάν πρόσβαση στο DeepL (10.000 χαρακτήρες/ημέρα)", "AI Translations (per day)": "Μεταφράσεις AI (ανά ημέρα)", - "Cloud Storage": "Αποθήκευση στο cloud", "Plus Plan": "Πρόγραμμα Plus", "Includes All Free Plan Benefits": "Περιλαμβάνει όλα τα οφέλη του δωρεάν προγράμματος", "Basic AI Translations": "Βασικές μεταφράσεις AI", @@ -399,5 +396,8 @@ "Pro Plan": "Πρόγραμμα Pro", "Includes All Plus Plan Benefits": "Περιλαμβάνει όλα τα οφέλη του Plus προγράμματος", "More AI Translations": "Περισσότερες μεταφράσεις AI", - "DeepL Pro Access (200K characters/day)": "Pro πρόσβαση στο DeepL (200.000 χαρακτήρες/ημέρα)" + "DeepL Pro Access (200K characters/day)": "Pro πρόσβαση στο DeepL (200.000 χαρακτήρες/ημέρα)", + "Complete Your Subscription": "Ολοκληρώστε τη συνδρομή σας", + "{{percentage}}% of Cloud Sync Space Used.": "{{percentage}}% του χώρου συγχρονισμού στο cloud χρησιμοποιήθηκε.", + "Cloud Sync Storage": "Χώρος συγχρονισμού cloud" } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index 09c48c1f..72fbae8b 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -126,8 +126,6 @@ "Failed to download book: {{title}}": "Error al descargar libro: {{title}}", "Upload Book": "Subir libro", "Auto Upload Books to Cloud": "Subir libros automáticamente a la nube", - "{{percentage}}% of Cloud Storage Used.": "{{percentage}}% del almacenamiento en la nube utilizado.", - "Storage": "Almacenamiento", "Book deleted: {{title}}": "Libro eliminado: {{title}}", "Failed to delete book: {{title}}": "Error al eliminar libro: {{title}}", "Check Updates on Start": "Comprobar actualizaciones al iniciar", @@ -393,7 +391,6 @@ "Unlimited Text-to-Speech Hours": "Horas ilimitadas de texto a voz", "DeepL Free Access (10K characters/day)": "Acceso gratuito a DeepL (10.000 caracteres/día)", "AI Translations (per day)": "Traducciones con IA (por día)", - "Cloud Storage": "Almacenamiento en la nube", "Plus Plan": "Plan Plus", "Includes All Free Plan Benefits": "Incluye todos los beneficios del plan gratuito", "Basic AI Translations": "Traducciones básicas con IA", @@ -403,5 +400,8 @@ "Includes All Plus Plan Benefits": "Incluye todos los beneficios del plan Plus", "More AI Translations": "Más traducciones con IA", "DeepL Pro Access (200K characters/day)": "Acceso Pro a DeepL (200.000 caracteres/día)", - "Your payment is being processed. This usually takes a few moments.": "Tu pago se está procesando. Normalmente tarda unos momentos." + "Your payment is being processed. This usually takes a few moments.": "Tu pago se está procesando. Normalmente tarda unos momentos.", + "Complete Your Subscription": "Completa tu suscripción", + "{{percentage}}% of Cloud Sync Space Used.": "{{percentage}}% del espacio de sincronización en la nube utilizado.", + "Cloud Sync Storage": "Almacenamiento en la nube" } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index 8a045484..529e6ce7 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -126,8 +126,6 @@ "Failed to download book: {{title}}": "Échec du téléchargement du livre : {{title}}", "Upload Book": "Télécharger un livre", "Auto Upload Books to Cloud": "Télécharger automatiquement les livres dans le cloud", - "{{percentage}}% of Cloud Storage Used.": "{{percentage}}% de l'espace de stockage dans le cloud utilisé.", - "Storage": "Stockage", "Book deleted: {{title}}": "Livre supprimé : {{title}}", "Failed to delete book: {{title}}": "Échec de la suppression du livre : {{title}}", "Check Updates on Start": "Vérifier les mises à jour au démarrage", @@ -394,7 +392,6 @@ "Unlimited Text-to-Speech Hours": "Heures illimitées de synthèse vocale", "DeepL Free Access (10K characters/day)": "Accès gratuit à DeepL (10 000 caractères/jour)", "AI Translations (per day)": "Traductions IA (par jour)", - "Cloud Storage": "Stockage cloud", "Plus Plan": "Abonnement Plus", "Includes All Free Plan Benefits": "Inclut tous les avantages de l’abonnement gratuit", "Basic AI Translations": "Traductions IA de base", @@ -403,5 +400,8 @@ "Pro Plan": "Abonnement Pro", "Includes All Plus Plan Benefits": "Inclut tous les avantages de l’abonnement Plus", "More AI Translations": "Plus de traductions IA", - "DeepL Pro Access (200K characters/day)": "Accès Pro à DeepL (200 000 caractères/jour)" + "DeepL Pro Access (200K characters/day)": "Accès Pro à DeepL (200 000 caractères/jour)", + "Complete Your Subscription": "Complétez votre abonnement", + "{{percentage}}% of Cloud Sync Space Used.": "{{percentage}}% de l’espace de synchronisation cloud utilisé.", + "Cloud Sync Storage": "Stockage de synchronisation cloud" } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index f36bce34..1b123fe7 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -126,8 +126,6 @@ "Failed to download book: {{title}}": "पुस्तक डाउनलोड करने में विफल: {{title}}", "Upload Book": "पुस्तक अपलोड करें", "Auto Upload Books to Cloud": "पुस्तकें बादल में स्वचालित रूप से अपलोड करें", - "{{percentage}}% of Cloud Storage Used.": "{{percentage}}% क्लाउड स्टोरेज का उपयोग किया गया है।", - "Storage": "स्टोरेज", "Book deleted: {{title}}": "पुस्तक हटाई गई: {{title}}", "Failed to delete book: {{title}}": "पुस्तक हटाने में विफल: {{title}}", "Check Updates on Start": "शुरू में अपडेट जांचें", @@ -390,7 +388,6 @@ "Unlimited Text-to-Speech Hours": "अनलिमिटेड पाठ-पाठ ध्वनि घंटे", "DeepL Free Access (10K characters/day)": "DeepL मुफ्त एक्सेस (10,000 अक्षर/दिन)", "AI Translations (per day)": "AI अनुवाद (प्रतिदिन)", - "Cloud Storage": "क्लाउड स्टोरेज", "Plus Plan": "Plus योजना", "Includes All Free Plan Benefits": "सभी मुफ्त योजना के लाभ शामिल हैं", "Basic AI Translations": "मूल AI अनुवाद", @@ -399,5 +396,8 @@ "Pro Plan": "Pro योजना", "Includes All Plus Plan Benefits": "सभी Plus योजना के लाभ शामिल हैं", "More AI Translations": "और अधिक AI अनुवाद", - "DeepL Pro Access (200K characters/day)": "DeepL प्रो एक्सेस (200,000 अक्षर/दिन)" + "DeepL Pro Access (200K characters/day)": "DeepL प्रो एक्सेस (200,000 अक्षर/दिन)", + "Complete Your Subscription": "अपनी सदस्यता पूरी करें", + "{{percentage}}% of Cloud Sync Space Used.": "{{percentage}}% क्लाउड सिंक स्पेस का उपयोग किया गया है।", + "Cloud Sync Storage": "क्लाउड सिंक स्टोरेज" } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index cbf9d8fa..af0b7c15 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -126,8 +126,6 @@ "Failed to download book: {{title}}": "Gagal mengunduh buku: {{title}}", "Upload Book": "Unggah Buku", "Auto Upload Books to Cloud": "Unggah Buku Otomatis ke Cloud", - "{{percentage}}% of Cloud Storage Used.": "{{percentage}}% dari Penyimpanan Cloud Digunakan.", - "Storage": "Penyimpanan", "Book deleted: {{title}}": "Buku dihapus: {{title}}", "Failed to delete book: {{title}}": "Gagal menghapus buku: {{title}}", "Check Updates on Start": "Periksa Pembaruan saat Memulai", @@ -386,7 +384,6 @@ "Unlimited Text-to-Speech Hours": "Jam Text-to-Speech Tanpa Batas", "DeepL Free Access (10K characters/day)": "Akses DeepL Gratis (10.000 karakter/hari)", "AI Translations (per day)": "Terjemahan AI (per hari)", - "Cloud Storage": "Penyimpanan Cloud", "Plus Plan": "Paket Plus", "Includes All Free Plan Benefits": "Termasuk Semua Manfaat Paket Gratis", "Basic AI Translations": "Terjemahan AI Dasar", @@ -395,5 +392,8 @@ "Pro Plan": "Paket Pro", "Includes All Plus Plan Benefits": "Termasuk Semua Manfaat Paket Plus", "More AI Translations": "Lebih Banyak Terjemahan AI", - "DeepL Pro Access (200K characters/day)": "Akses DeepL Pro (200.000 karakter/hari)" + "DeepL Pro Access (200K characters/day)": "Akses DeepL Pro (200.000 karakter/hari)", + "Complete Your Subscription": "Selesaikan Langganan Anda", + "{{percentage}}% of Cloud Sync Space Used.": "{{percentage}}% ruang sinkronisasi cloud terpakai", + "Cloud Sync Storage": "Ruang cloud" } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index 813f7a70..9ee786d5 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -126,8 +126,6 @@ "Failed to download book: {{title}}": "Download libro non riuscito: {{title}}", "Upload Book": "Carica libro", "Auto Upload Books to Cloud": "Carica libri automaticamente su cloud", - "{{percentage}}% of Cloud Storage Used.": "{{percentage}}% dello spazio di archiviazione cloud utilizzato.", - "Storage": "Archiviazione", "Book deleted: {{title}}": "Libro eliminato: {{title}}", "Failed to delete book: {{title}}": "Eliminazione libro non riuscita: {{title}}", "Check Updates on Start": "Controlla aggiornamenti all'avvio", @@ -394,7 +392,6 @@ "Unlimited Text-to-Speech Hours": "Ore Illimitate di Sintesi Vocale", "DeepL Free Access (10K characters/day)": "Accesso Gratuito a DeepL (10.000 caratteri/giorno)", "AI Translations (per day)": "Traduzioni AI (al giorno)", - "Cloud Storage": "Archiviazione Cloud", "Plus Plan": "Piano Plus", "Includes All Free Plan Benefits": "Include Tutti i Vantaggi del Piano Gratuito", "Basic AI Translations": "Traduzioni AI di Base", @@ -403,5 +400,8 @@ "Pro Plan": "Piano Pro", "Includes All Plus Plan Benefits": "Include Tutti i Vantaggi del Piano Plus", "More AI Translations": "Più Traduzioni AI", - "DeepL Pro Access (200K characters/day)": "Accesso DeepL Pro (200.000 caratteri/giorno)" + "DeepL Pro Access (200K characters/day)": "Accesso DeepL Pro (200.000 caratteri/giorno)", + "Complete Your Subscription": "Completa l'abbonamento", + "{{percentage}}% of Cloud Sync Space Used.": "{{percentage}}% dello spazio cloud usato", + "Cloud Sync Storage": "Spazio cloud" } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index 457369e7..f432d077 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -126,8 +126,6 @@ "Failed to download book: {{title}}": "書籍のダウンロードに失敗しました:{{title}}", "Upload Book": "書籍をアップロード", "Auto Upload Books to Cloud": "書籍を自動的にクラウドにアップロード", - "{{percentage}}% of Cloud Storage Used.": "クラウドストレージの{{percentage}}%が使用されています。", - "Storage": "ストレージ", "Book deleted: {{title}}": "書籍が削除されました:{{title}}", "Failed to delete book: {{title}}": "書籍の削除に失敗しました:{{title}}", "Check Updates on Start": "開始時に更新を確認", @@ -386,7 +384,6 @@ "Unlimited Text-to-Speech Hours": "無制限の音声読み上げ時間", "DeepL Free Access (10K characters/day)": "DeepL無料アクセス(1日1万文字)", "AI Translations (per day)": "AI翻訳(1日あたり)", - "Cloud Storage": "クラウドストレージ", "Plus Plan": "Plusプラン", "Includes All Free Plan Benefits": "無料プランの全特典を含む", "Basic AI Translations": "基本的なAI翻訳", @@ -395,5 +392,8 @@ "Pro Plan": "Proプラン", "Includes All Plus Plan Benefits": "Plusプランの全特典を含む", "More AI Translations": "さらに多くのAI翻訳", - "DeepL Pro Access (200K characters/day)": "DeepL Proアクセス(1日20万文字)" + "DeepL Pro Access (200K characters/day)": "DeepL Proアクセス(1日20万文字)", + "Complete Your Subscription": "サブスクリプションを完了する", + "{{percentage}}% of Cloud Sync Space Used.": "クラウド同期スペースの使用量は{{percentage}}%です。", + "Cloud Sync Storage": "クラウド同期容量" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index 980e2f44..d5475588 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -126,8 +126,6 @@ "Failed to download book: {{title}}": "책 다운로드 실패: {{title}}", "Upload Book": "책 업로드", "Auto Upload Books to Cloud": "책 자동으로 클라우드에 업로드", - "{{percentage}}% of Cloud Storage Used.": "클라우드 저장소 사용량 {{percentage}}%.", - "Storage": "저장소", "Book deleted: {{title}}": "책 삭제됨: {{title}}", "Failed to delete book: {{title}}": "책 삭제 실패: {{title}}", "Check Updates on Start": "시작 시 업데이트 확인", @@ -386,7 +384,6 @@ "Unlimited Text-to-Speech Hours": "무제한 음성 변환 시간", "DeepL Free Access (10K characters/day)": "DeepL 무료 액세스 (일 1만자)", "AI Translations (per day)": "AI 번역 (일 기준)", - "Cloud Storage": "클라우드 저장소", "Plus Plan": "Plus 요금제", "Includes All Free Plan Benefits": "무료 요금제의 모든 혜택 포함", "Basic AI Translations": "기본 AI 번역", @@ -395,5 +392,8 @@ "Pro Plan": "Pro 요금제", "Includes All Plus Plan Benefits": "Plus 요금제의 모든 혜택 포함", "More AI Translations": "더 많은 AI 번역", - "DeepL Pro Access (200K characters/day)": "DeepL Pro 액세스 (일 20만자)" + "DeepL Pro Access (200K characters/day)": "DeepL Pro 액세스 (일 20만자)", + "Complete Your Subscription": "구독 완료", + "{{percentage}}% of Cloud Sync Space Used.": "{{percentage}}%의 클라우드 동기화 공간 사용됨.", + "Cloud Sync Storage": "클라우드 저장 공간" } diff --git a/apps/readest-app/public/locales/nl/translation.json b/apps/readest-app/public/locales/nl/translation.json index 010ea8ff..ba78e38a 100644 --- a/apps/readest-app/public/locales/nl/translation.json +++ b/apps/readest-app/public/locales/nl/translation.json @@ -57,8 +57,6 @@ "Import Books": "Boeken importeren", "Select Multiple Books": "Meerdere boeken selecteren", "Select Books": "Boeken selecteren", - "Storage": "Opslag", - "{{percentage}}% of Cloud Storage Used.": "{{percentage}}% van cloudopslag gebruikt.", "Logged in as {{userDisplayName}}": "Ingelogd als {{userDisplayName}}", "Logged in": "Ingelogd", "Account": "Account", @@ -388,7 +386,6 @@ "Unlimited Text-to-Speech Hours": "Onbeperkte tekst-naar-spraak-uren", "DeepL Free Access (10K characters/day)": "DeepL gratis toegang (10.000 tekens/dag)", "AI Translations (per day)": "AI-vertalingen (per dag)", - "Cloud Storage": "Cloudopslag", "Plus Plan": "Plus-abonnement", "Includes All Free Plan Benefits": "Bevat alle voordelen van het gratis abonnement", "Basic AI Translations": "Basis AI-vertalingen", @@ -399,5 +396,8 @@ "More AI Translations": "Meer AI-vertalingen", "DeepL Pro Access (200K characters/day)": "DeepL Pro toegang (200.000 tekens/dag)", "We couldn't process your subscription. Please try again or contact support if the issue persists.": "We konden je abonnement niet verwerken. Probeer het opnieuw of neem contact op met support als het probleem blijft bestaan.", - "Thank you for your subscription! Your payment has been processed successfully.": "Bedankt voor je abonnement! Je betaling is succesvol verwerkt." + "Thank you for your subscription! Your payment has been processed successfully.": "Bedankt voor je abonnement! Je betaling is succesvol verwerkt.", + "Complete Your Subscription": "Voltooi uw abonnement", + "{{percentage}}% of Cloud Sync Space Used.": "{{percentage}}% van de cloud-synchronisatie ruimte gebruikt.", + "Cloud Sync Storage": "Cloud Sync Opslag" } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index 8fb53493..f18ac07a 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -126,8 +126,6 @@ "Failed to download book: {{title}}": "Nie udało się pobrać książki: {{title}}", "Upload Book": "Prześlij książkę", "Auto Upload Books to Cloud": "Automatycznie przesyłaj książki do chmury", - "{{percentage}}% of Cloud Storage Used.": "{{percentage}}% przestrzeni dyskowej w chmurze użytej.", - "Storage": "Przechowywanie", "Book deleted: {{title}}": "Książka usunięta: {{title}}", "Failed to delete book: {{title}}": "Nie udało się usunąć książki: {{title}}", "Check Updates on Start": "Sprawdź aktualizacje przy starcie", @@ -398,7 +396,6 @@ "Unlimited Text-to-Speech Hours": "Nieograniczone godziny zamiany tekstu na mowę", "DeepL Free Access (10K characters/day)": "Darmowy dostęp do DeepL (10 tys. znaków/dzień)", "AI Translations (per day)": "Tłumaczenia AI (na dzień)", - "Cloud Storage": "Przechowywanie w chmurze", "Plus Plan": "Plan Plus", "Includes All Free Plan Benefits": "Zawiera wszystkie korzyści planu darmowego", "Basic AI Translations": "Podstawowe tłumaczenia AI", @@ -407,5 +404,8 @@ "Pro Plan": "Plan Pro", "Includes All Plus Plan Benefits": "Zawiera wszystkie korzyści planu Plus", "More AI Translations": "Więcej tłumaczeń AI", - "DeepL Pro Access (200K characters/day)": "DeepL Pro (200 tys. znaków/dzień)" + "DeepL Pro Access (200K characters/day)": "DeepL Pro (200 tys. znaków/dzień)", + "Complete Your Subscription": "Ukończ subskrypcję", + "{{percentage}}% of Cloud Sync Space Used.": "Użyto {{percentage}}% przestrzeni synchronizacji w chmurze.", + "Cloud Sync Storage": "Miejsce w chmurze" } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index 07e526a1..7b5523ef 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -126,8 +126,6 @@ "Failed to download book: {{title}}": "Falha ao baixar livro: {{title}}", "Upload Book": "Enviar Livro", "Auto Upload Books to Cloud": "Enviar Livros para a Nuvem Automaticamente", - "{{percentage}}% of Cloud Storage Used.": "{{percentage}}% do Armazenamento na Nuvem Usado.", - "Storage": "Armazenamento", "Book deleted: {{title}}": "Livro excluído: {{title}}", "Failed to delete book: {{title}}": "Falha ao excluir livro: {{title}}", "Check Updates on Start": "Verificar Atualizações ao Iniciar", @@ -394,7 +392,6 @@ "Unlimited Text-to-Speech Hours": "Horas ilimitadas de texto para fala", "DeepL Free Access (10K characters/day)": "Acesso gratuito ao DeepL (10 mil caracteres/dia)", "AI Translations (per day)": "Traduções com IA (por dia)", - "Cloud Storage": "Armazenamento na nuvem", "Plus Plan": "Plano Plus", "Includes All Free Plan Benefits": "Inclui todos os benefícios do plano gratuito", "Basic AI Translations": "Traduções básicas com IA", @@ -403,5 +400,8 @@ "Pro Plan": "Plano Pro", "Includes All Plus Plan Benefits": "Inclui todos os benefícios do plano Plus", "More AI Translations": "Mais traduções com IA", - "DeepL Pro Access (200K characters/day)": "Acesso ao DeepL Pro (200 mil caracteres/dia)" + "DeepL Pro Access (200K characters/day)": "Acesso ao DeepL Pro (200 mil caracteres/dia)", + "Complete Your Subscription": "Complete sua assinatura", + "{{percentage}}% of Cloud Sync Space Used.": "{{percentage}}% do espaço de sincronização na nuvem utilizado.", + "Cloud Sync Storage": "Armazenamento na Nuvem" } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index 9783dfd4..4d6ca9dd 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -126,8 +126,6 @@ "Failed to download book: {{title}}": "Не удалось скачать книгу: {{title}}", "Upload Book": "Загрузить книгу", "Auto Upload Books to Cloud": "Автоматическая загрузка книг в облако", - "{{percentage}}% of Cloud Storage Used.": "{{percentage}}% использовано облачного хранилища.", - "Storage": "Хранилище", "Book deleted: {{title}}": "Книга удалена: {{title}}", "Failed to delete book: {{title}}": "Не удалось удалить книгу: {{title}}", "Check Updates on Start": "Проверять обновления при запуске", @@ -398,7 +396,6 @@ "Unlimited Text-to-Speech Hours": "Неограниченные часы озвучивания текста", "DeepL Free Access (10K characters/day)": "Бесплатный доступ к DeepL (10 тыс. символов/день)", "AI Translations (per day)": "Переводы ИИ (в день)", - "Cloud Storage": "Облачное хранилище", "Plus Plan": "План Plus", "Includes All Free Plan Benefits": "Включает все преимущества бесплатного плана", "Basic AI Translations": "Базовые переводы ИИ", @@ -407,5 +404,8 @@ "Pro Plan": "План Pro", "Includes All Plus Plan Benefits": "Включает все преимущества плана Plus", "More AI Translations": "Больше переводов ИИ", - "DeepL Pro Access (200K characters/day)": "Доступ к DeepL Pro (200 тыс. символов/день)" + "DeepL Pro Access (200K characters/day)": "Доступ к DeepL Pro (200 тыс. символов/день)", + "Complete Your Subscription": "Завершите подписку", + "{{percentage}}% of Cloud Sync Space Used.": "Использовано {{percentage}}% облачного пространства для синхронизации.", + "Cloud Sync Storage": "Облачное хранилище" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index a3f2cdb7..210c9640 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -126,8 +126,6 @@ "Failed to download book: {{title}}": "Kitap indirilemedi: {{title}}", "Upload Book": "Kitap Yükle", "Auto Upload Books to Cloud": "Kitapları Otomatik Olarak Buluta Yükle", - "{{percentage}}% of Cloud Storage Used.": "Bulut Depolamanın %{{percentage}}'si kullanıldı.", - "Storage": "Depolama", "Book deleted: {{title}}": "Kitap silindi: {{title}}", "Failed to delete book: {{title}}": "Kitap silinemedi: {{title}}", "Check Updates on Start": "Başlangıçta Güncellemeleri Kontrol Et", @@ -390,7 +388,6 @@ "Unlimited Text-to-Speech Hours": "Sınırsız Sesli Okuma Süresi", "DeepL Free Access (10K characters/day)": "DeepL Ücretsiz Erişim (Günde 10K karakter)", "AI Translations (per day)": "AI Çevirileri (günlük)", - "Cloud Storage": "Bulut Depolama", "Plus Plan": "Plus Plan", "Includes All Free Plan Benefits": "Tüm Ücretsiz Plan Özelliklerini İçerir", "Basic AI Translations": "Temel AI Çevirileri", @@ -399,5 +396,8 @@ "Pro Plan": "Pro Plan", "Includes All Plus Plan Benefits": "Tüm Plus Plan Özelliklerini İçerir", "More AI Translations": "Daha Fazla AI Çevirisi", - "DeepL Pro Access (200K characters/day)": "DeepL Pro Erişim (Günde 200K karakter)" + "DeepL Pro Access (200K characters/day)": "DeepL Pro Erişim (Günde 200K karakter)", + "Complete Your Subscription": "Aboneliğinizi Tamamlayın", + "{{percentage}}% of Cloud Sync Space Used.": "Bulut Senkronizasyon Alanının %{{percentage}}'si Kullanıldı.", + "Cloud Sync Storage": "Bulut Senkronizasyon Depolama" } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index 625da85c..46c52ef3 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -126,8 +126,6 @@ "Failed to download book: {{title}}": "Не вдалося завантажити книгу: {{title}}", "Upload Book": "Завантажити книгу", "Auto Upload Books to Cloud": "Автоматично завантажувати книги в хмару", - "{{percentage}}% of Cloud Storage Used.": "{{percentage}}% використано хмарного сховища.", - "Storage": "Сховище", "Book deleted: {{title}}": "Книгу видалено: {{title}}", "Failed to delete book: {{title}}": "Не вдалося видалити книгу: {{title}}", "Check Updates on Start": "Перевіряти оновлення при запуску", @@ -398,7 +396,6 @@ "Unlimited Text-to-Speech Hours": "Необмежені години озвучення тексту", "DeepL Free Access (10K characters/day)": "Безкоштовний доступ до DeepL (10 тис. символів/день)", "AI Translations (per day)": "AI-переклади (на день)", - "Cloud Storage": "Хмарне сховище", "Plus Plan": "План Plus", "Includes All Free Plan Benefits": "Включає всі переваги Безкоштовного плану", "Basic AI Translations": "Базові AI-переклади", @@ -407,5 +404,8 @@ "Pro Plan": "План Pro", "Includes All Plus Plan Benefits": "Включає всі переваги плану Plus", "More AI Translations": "Більше AI-перекладів", - "DeepL Pro Access (200K characters/day)": "DeepL Pro доступ (200 тис. символів/день)" + "DeepL Pro Access (200K characters/day)": "DeepL Pro доступ (200 тис. символів/день)", + "Complete Your Subscription": "Завершіть підписку", + "{{percentage}}% of Cloud Sync Space Used.": "Використано {{percentage}}% хмарного сховища.", + "Cloud Sync Storage": "Облачне хранилище" } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index 0b916173..c074a752 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -126,8 +126,6 @@ "Failed to download book: {{title}}": "Không thể tải về sách: {{title}}", "Upload Book": "Tải lên sách", "Auto Upload Books to Cloud": "Tự động tải sách lên đám mây", - "{{percentage}}% of Cloud Storage Used.": "{{percentage}}% dung lượng lưu trữ đám mây đã sử dụng.", - "Storage": "Lưu trữ", "Book deleted: {{title}}": "Sách đã xóa: {{title}}", "Failed to delete book: {{title}}": "Không thể xóa sách: {{title}}", "Check Updates on Start": "Kiểm tra cập nhật khi khởi động", @@ -386,7 +384,6 @@ "Unlimited Text-to-Speech Hours": "Không giới hạn giờ chuyển văn bản thành giọng nói", "DeepL Free Access (10K characters/day)": "Truy cập DeepL miễn phí (10.000 ký tự/ngày)", "AI Translations (per day)": "Dịch AI (mỗi ngày)", - "Cloud Storage": "Lưu trữ đám mây", "Plus Plan": "Gói Plus", "Includes All Free Plan Benefits": "Bao gồm tất cả lợi ích của gói miễn phí", "Basic AI Translations": "Dịch AI cơ bản", @@ -395,5 +392,8 @@ "Pro Plan": "Gói Pro", "Includes All Plus Plan Benefits": "Bao gồm tất cả lợi ích của gói Plus", "More AI Translations": "Thêm dịch AI", - "DeepL Pro Access (200K characters/day)": "Truy cập DeepL Pro (200.000 ký tự/ngày)" + "DeepL Pro Access (200K characters/day)": "Truy cập DeepL Pro (200.000 ký tự/ngày)", + "Complete Your Subscription": "Hoàn tất đăng ký của bạn", + "{{percentage}}% of Cloud Sync Space Used.": "Đã sử dụng {{percentage}}% không gian đồng bộ đám mây.", + "Cloud Sync Storage": "Lưu trữ đồng bộ đám mây" } diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json index 0773a1c9..f81dcd52 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -126,8 +126,6 @@ "Failed to download book: {{title}}": "下载书籍失败:{{title}}", "Upload Book": "上传书籍", "Auto Upload Books to Cloud": "自动上传书籍到云端", - "{{percentage}}% of Cloud Storage Used.": "已使用 {{percentage}}% 云存储空间", - "Storage": "存储空间", "Book deleted: {{title}}": "书籍已删除:{{title}}", "Failed to delete book: {{title}}": "删除书籍失败:{{title}}", "Check Updates on Start": "启动时检查更新", @@ -386,7 +384,6 @@ "Unlimited Text-to-Speech Hours": "无限文本转语音时长", "DeepL Free Access (10K characters/day)": "DeepL 免费额度(每日1万字符)", "AI Translations (per day)": "AI 翻译(每日)", - "Cloud Storage": "云存储", "Plus Plan": "Plus 套餐", "Includes All Free Plan Benefits": "包含所有免费套餐功能", "Basic AI Translations": "基础 AI 翻译", @@ -395,5 +392,8 @@ "Pro Plan": "Pro 套餐", "Includes All Plus Plan Benefits": "包含所有 Plus 套餐功能", "More AI Translations": "更多 AI 翻译", - "DeepL Pro Access (200K characters/day)": "DeepL Pro(每日20万字符)" + "DeepL Pro Access (200K characters/day)": "DeepL Pro(每日20万字符)", + "Complete Your Subscription": "完成您的订阅", + "{{percentage}}% of Cloud Sync Space Used.": "已使用 {{percentage}}% 的云同步空间。", + "Cloud Sync Storage": "云同步空间" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index cbbf284e..80821883 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -126,8 +126,6 @@ "Failed to download book: {{title}}": "書籍下載失敗:{{title}}", "Upload Book": "上傳書籍", "Auto Upload Books to Cloud": "自動上傳書籍到雲端", - "{{percentage}}% of Cloud Storage Used.": "已使用 {{percentage}}% 雲存儲空間", - "Storage": "存儲空間", "Book deleted: {{title}}": "書籍已刪除:{{title}}", "Failed to delete book: {{title}}": "書籍刪除失敗:{{title}}", "Check Updates on Start": "啟動時檢查更新", @@ -386,7 +384,6 @@ "Unlimited Text-to-Speech Hours": "無限文字轉語音時數", "DeepL Free Access (10K characters/day)": "DeepL 免費使用(每日1萬字)", "AI Translations (per day)": "AI 翻譯(每日)", - "Cloud Storage": "雲端儲存", "Plus Plan": "Plus 方案", "Includes All Free Plan Benefits": "包含所有免費方案功能", "Basic AI Translations": "基本 AI 翻譯", @@ -395,5 +392,8 @@ "Pro Plan": "Pro 方案", "Includes All Plus Plan Benefits": "包含所有 Plus 方案功能", "More AI Translations": "更多 AI 翻譯", - "DeepL Pro Access (200K characters/day)": "DeepL Pro(每日20萬字)" + "DeepL Pro Access (200K characters/day)": "DeepL Pro(每日20萬字)", + "Complete Your Subscription": "完成您的訂閱", + "{{percentage}}% of Cloud Sync Space Used.": "已使用 {{percentage}}% 的雲同步空間。", + "Cloud Sync Storage": "雲同步空間" } diff --git a/apps/readest-app/src/app/api/stripe/checkout/route.ts b/apps/readest-app/src/app/api/stripe/checkout/route.ts index 2c3667cc..7c3a1556 100644 --- a/apps/readest-app/src/app/api/stripe/checkout/route.ts +++ b/apps/readest-app/src/app/api/stripe/checkout/route.ts @@ -4,7 +4,7 @@ import { validateUserAndToken } from '@/utils/access'; import { supabase } from '@/utils/supabase'; export async function POST(request: NextRequest) { - const { priceId, metadata = {} } = await request.json(); + const { priceId, embedded = true, metadata = {} } = await request.json(); const { user, token } = await validateUserAndToken(request.headers.get('authorization')); if (!user || !token) { @@ -42,7 +42,10 @@ export async function POST(request: NextRequest) { } const stripe = getStripe(); + const successUrl = `${request.headers.get('origin')}/user/subscription/success?session_id={CHECKOUT_SESSION_ID}`; + const returnUrl = `${request.headers.get('origin')}/user`; const session = await stripe.checkout.sessions.create({ + ui_mode: embedded ? 'embedded' : 'hosted', customer: customerId, mode: 'subscription', payment_method_types: ['card'], @@ -53,11 +56,16 @@ export async function POST(request: NextRequest) { }, ], metadata: enhancedMetadata, - success_url: `${request.headers.get('origin')}/user/subscription/success?session_id={CHECKOUT_SESSION_ID}`, - cancel_url: `${request.headers.get('origin')}/user`, + success_url: embedded ? undefined : successUrl, + cancel_url: embedded ? undefined : returnUrl, + redirect_on_completion: embedded ? 'never' : undefined, }); - return NextResponse.json({ sessionId: session.id, url: session.url }); + return NextResponse.json({ + url: session.url, + sessionId: session.id, + clientSecret: session.client_secret, + }); } catch (error) { console.error(error); return NextResponse.json({ error: 'Error creating checkout session' }, { status: 500 }); diff --git a/apps/readest-app/src/app/user/components/Checkout.tsx b/apps/readest-app/src/app/user/components/Checkout.tsx new file mode 100644 index 00000000..5d662459 --- /dev/null +++ b/apps/readest-app/src/app/user/components/Checkout.tsx @@ -0,0 +1,53 @@ +'use client'; + +import clsx from 'clsx'; +import React, { useCallback } from 'react'; +import { EmbeddedCheckoutProvider, EmbeddedCheckout } from '@stripe/react-stripe-js'; +import { getStripe } from '@/libs/stripe/client'; +import { useTranslation } from '@/hooks/useTranslation'; + +interface CheckoutProps { + clientSecret: string; + sessionId: string; + planName?: string; + className?: string; + onSuccess?: (sessionId: string) => void; +} + +const Checkout: React.FC = ({ + clientSecret, + sessionId, + planName, + onSuccess, + className = '', +}) => { + const _ = useTranslation(); + const stripe = getStripe(); + + const options = { + clientSecret, + onComplete: useCallback(() => { + onSuccess?.(sessionId); + }, [onSuccess, sessionId]), + }; + + return ( +
+
+

+ {planName + ? _('Upgrade to {{plan}}', { plan: _(planName) }) + : _('Complete Your Subscription')} +

+
+ +
+ + + +
+
+ ); +}; + +export default Checkout; diff --git a/apps/readest-app/src/app/user/components/PlansComparison.tsx b/apps/readest-app/src/app/user/components/PlansComparison.tsx index c5964653..61605c25 100644 --- a/apps/readest-app/src/app/user/components/PlansComparison.tsx +++ b/apps/readest-app/src/app/user/components/PlansComparison.tsx @@ -1,9 +1,9 @@ import { useCallback, useEffect, useRef, useState } from 'react'; +import { useEnv } from '@/context/EnvContext'; import { debounce } from '@/utils/debounce'; -import { isWebAppPlatform } from '@/services/environment'; import { getPlanDetails } from '../utils/plan'; -import { AvailablePlan } from '../page'; import { UserPlan } from '@/types/user'; +import { AvailablePlan } from '../page'; import PlanNavigation from './PlanNavigation'; import PlanCard from './PlanCard'; import PlanIndicators from './PlanIndicators'; @@ -19,6 +19,7 @@ const PlansComparison: React.FC = ({ userPlan, onSubscribe, }) => { + const { appService } = useEnv(); const [currentPlanIndex, setCurrentPlanIndex] = useState(0); const [userPlanIndex, setUserPlanIndex] = useState(0); const plansScrollRef = useRef(null); @@ -137,7 +138,7 @@ const PlansComparison: React.FC = ({ userPlanIndex} index={index} diff --git a/apps/readest-app/src/app/user/page.tsx b/apps/readest-app/src/app/user/page.tsx index e3e63b78..cde5e406 100644 --- a/apps/readest-app/src/app/user/page.tsx +++ b/apps/readest-app/src/app/user/page.tsx @@ -2,7 +2,8 @@ import clsx from 'clsx'; import Stripe from 'stripe'; -import React, { useState, useEffect } from 'react'; +import posthog from 'posthog-js'; +import React, { useState, useEffect, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { useEnv } from '@/context/EnvContext'; import { useAuth } from '@/context/AuthContext'; @@ -15,15 +16,18 @@ import { navigateToLibrary } from '@/utils/nav'; import { deleteUser } from '@/libs/user'; import { eventDispatcher } from '@/utils/event'; import { getStripe } from '@/libs/stripe/client'; -import { getAPIBaseUrl } from '@/services/environment'; +import { getAPIBaseUrl, isTauriAppPlatform, isWebAppPlatform } from '@/services/environment'; +import { openUrl } from '@tauri-apps/plugin-opener'; import { getAccessToken } from '@/utils/access'; import { getPlanDetails } from './utils/plan'; import { Toast } from '@/components/Toast'; +import Spinner from '@/components/Spinner'; import ProfileHeader from './components/Header'; import UserInfo from './components/UserInfo'; import UsageStats from './components/UsageStats'; import PlansComparison from './components/PlansComparison'; import AccountActions from './components/AccountActions'; +import Checkout from './components/Checkout'; const WEB_STRIPE_PLANS_URL = `${getAPIBaseUrl()}/stripe/plans`; const WEB_STRIPE_CHECKOUT_URL = `${getAPIBaseUrl()}/stripe/checkout`; @@ -38,6 +42,12 @@ export type AvailablePlan = { product: Stripe.Product; }; +type CheckoutState = { + clientSecret: string; + sessionId: string; + planName: string; +}; + const ProfilePage = () => { const _ = useTranslation(); const router = useRouter(); @@ -45,7 +55,14 @@ const ProfilePage = () => { const { token, user, logout } = useAuth(); const { settings, setSettings, saveSettings } = useSettingsStore(); + const [loading, setLoading] = useState(false); const [availablePlans, setAvailablePlans] = useState([]); + const [showEmbeddedCheckout, setShowEmbeddedCheckout] = useState(false); + const [checkoutState, setCheckoutState] = useState({ + clientSecret: '', + sessionId: '', + planName: '', + }); const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); @@ -55,7 +72,11 @@ const ProfilePage = () => { const { quotas, userPlan } = useQuotaStats(); const handleGoBack = () => { - navigateToLibrary(router); + if (showEmbeddedCheckout) { + setShowEmbeddedCheckout(false); + } else { + navigateToLibrary(router); + } }; const handleLogout = () => { @@ -86,26 +107,54 @@ const ProfilePage = () => { console.error('Stripe not loaded'); return; } - const { sessionId, url } = await fetch(WEB_STRIPE_CHECKOUT_URL, { + setLoading(true); + const isEmbeddedCheckout = isTauriAppPlatform(); + const { sessionId, clientSecret, url } = await fetch(WEB_STRIPE_CHECKOUT_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, - body: JSON.stringify({ priceId }), + body: JSON.stringify({ priceId, embedded: isEmbeddedCheckout }), }).then((res) => res.json()); + setLoading(false); - if (url) { - window.location.href = url; - } else { + const selectedPlan = availablePlans.find((plan) => plan.price_id === priceId)!; + const planName = selectedPlan.product.name; + if (isEmbeddedCheckout && sessionId && clientSecret) { + setShowEmbeddedCheckout(true); + setCheckoutState({ + planName, + clientSecret, + sessionId, + }); + } else if (url) { + if (isWebAppPlatform()) { + window.location.href = url; + } else if (isTauriAppPlatform()) { + await openUrl(url); + } + } else if (sessionId) { const result = await stripe.redirectToCheckout({ sessionId }); - if (result.error) { console.error(result.error); + posthog.capture('checkout_error', { + error: 'Failed to redirect to checkout', + }); } + } else { + console.error('No sessionId or url returned from checkout API'); + posthog.capture('checkout_error', { + error: 'No sessionId or url returned from checkout API', + }); } }; + const handleCheckoutSuccess = useCallback((sessionId: string) => { + setShowEmbeddedCheckout(false); + router.push(`/user/subscription/success?session_id=${sessionId}`); + }, []); + const handleManageSubscription = async () => { const token = await getAccessToken(); const response = await fetch(WEB_STRIPE_PORTAL_URL, { @@ -127,7 +176,11 @@ const ProfilePage = () => { return; } - window.location.href = url; + if (isWebAppPlatform()) { + window.location.href = url; + } else if (isTauriAppPlatform()) { + await openUrl(url); + } }; useEffect(() => { @@ -168,31 +221,47 @@ const ProfilePage = () => { >
-
-
- - - - - - - + +
+ )} + {showEmbeddedCheckout ? ( +
+
-
+ ) : ( +
+
+ + + + + + + +
+
+ )}
diff --git a/apps/readest-app/src/app/user/utils/plan.ts b/apps/readest-app/src/app/user/utils/plan.ts index 6dc5767b..8ea4a21d 100644 --- a/apps/readest-app/src/app/user/utils/plan.ts +++ b/apps/readest-app/src/app/user/utils/plan.ts @@ -35,8 +35,8 @@ export const getPlanDetails = ( _('500 MB Cloud Sync Space'), ], limits: { + [_('Cloud Sync Storage')]: '500 MB', [_('AI Translations (per day)')]: '10K', - [_('Cloud Storage')]: '500 MB', }, }; case 'plus': @@ -55,8 +55,8 @@ export const getPlanDetails = ( _('2 GB Cloud Sync Space'), ], limits: { + [_('Cloud Sync Storage')]: '2 GB', [_('AI Translations (per day)')]: '50K', - [_('Cloud Storage')]: '2 GB', }, }; case 'pro': @@ -76,8 +76,8 @@ export const getPlanDetails = ( _('10 GB Cloud Sync Space'), ], limits: { + [_('Cloud Sync Storage')]: '10 GB', [_('AI Translations (per day)')]: '200K', - [_('Cloud Storage')]: '10 GB', }, }; default: diff --git a/apps/readest-app/src/components/AboutWindow.tsx b/apps/readest-app/src/components/AboutWindow.tsx index be621224..0bc54761 100644 --- a/apps/readest-app/src/components/AboutWindow.tsx +++ b/apps/readest-app/src/components/AboutWindow.tsx @@ -1,10 +1,10 @@ import React, { useEffect, useState } from 'react'; import Image from 'next/image'; -import packageJson from '../../package.json'; import { useEnv } from '@/context/EnvContext'; import { useTranslation } from '@/hooks/useTranslation'; import { checkForAppUpdates } from '@/helpers/updater'; import { parseWebViewVersion } from '@/utils/ua'; +import { getAppVersion } from '@/utils/version'; import Dialog from './Dialog'; import Link from './Link'; @@ -83,7 +83,7 @@ export const AboutWindow = () => {

Readest

- {_('Version {{version}}', { version: packageJson.version })} {`(${browserInfo})`} + {_('Version {{version}}', { version: getAppVersion() })} {`(${browserInfo})`}

diff --git a/apps/readest-app/src/components/Providers.tsx b/apps/readest-app/src/components/Providers.tsx index 8d6dd907..8fa838e4 100644 --- a/apps/readest-app/src/components/Providers.tsx +++ b/apps/readest-app/src/components/Providers.tsx @@ -1,19 +1,15 @@ 'use client'; -import { useEffect, useState } from 'react'; import { IconContext } from 'react-icons'; import { AuthProvider } from '@/context/AuthContext'; import { EnvProvider } from '@/context/EnvContext'; import { CSPostHogProvider } from '@/context/PHContext'; import { SyncProvider } from '@/context/SyncContext'; import { useDefaultIconSize } from '@/hooks/useResponsiveSize'; -import { isWebAppPlatform } from '@/services/environment'; const Providers = ({ children }: { children: React.ReactNode }) => { const iconSize = useDefaultIconSize(); - const [mounted, setMounted] = useState(false); - useEffect(() => setMounted(true), []); - return mounted || isWebAppPlatform() ? ( + return ( @@ -23,7 +19,7 @@ const Providers = ({ children }: { children: React.ReactNode }) => { - ) : null; + ); }; export default Providers; diff --git a/apps/readest-app/src/components/UpdaterWindow.tsx b/apps/readest-app/src/components/UpdaterWindow.tsx index 80c4fe1b..d0ebfcfb 100644 --- a/apps/readest-app/src/components/UpdaterWindow.tsx +++ b/apps/readest-app/src/components/UpdaterWindow.tsx @@ -10,11 +10,11 @@ import { fetch } from '@tauri-apps/plugin-http'; import { useTranslator } from '@/hooks/useTranslator'; import { useTranslation } from '@/hooks/useTranslation'; import { useSearchParams } from 'next/navigation'; +import { getAppVersion } from '@/utils/version'; import { tauriDownload } from '@/utils/transfer'; import { installPackage } from '@/utils/bridge'; import { getLocale } from '@/utils/misc'; import { READEST_UPDATER_FILE, READEST_CHANGELOG_FILE } from '@/services/constants'; -import packageJson from '../../package.json'; import Dialog from '@/components/Dialog'; interface ReleaseNotes { @@ -68,7 +68,7 @@ export const UpdaterContent = ({ version }: { version?: string }) => { }); const { appService } = useEnv(); const searchParams = useSearchParams(); - const currentVersion = packageJson.version; + const currentVersion = getAppVersion(); const resolvedVersion = version ?? searchParams?.get('version') ?? ''; const [update, setUpdate] = useState(null); const [isMounted, setIsMounted] = useState(false); @@ -93,7 +93,7 @@ export const UpdaterContent = ({ version }: { version?: string }) => { const checkAndroidUpdate = async () => { const response = await fetch(READEST_UPDATER_FILE); const data = await response.json(); - if (semver.gt(data.version, packageJson.version)) { + if (semver.gt(data.version, currentVersion)) { const OS_ARCH = osArch(); const platformKey = OS_ARCH === 'aarch64' ? 'android-arm64' : 'android-universal'; const arch = OS_ARCH === 'aarch64' ? 'arm64' : 'universal'; @@ -101,7 +101,7 @@ export const UpdaterContent = ({ version }: { version?: string }) => { const cachePrefix = appService?.fs.getPrefix('Cache'); const apkFilePath = `${cachePrefix}/Readest_${data.version}_${arch}.apk`; setUpdate({ - currentVersion: packageJson.version, + currentVersion, version: data.version, date: data.date, body: data.notes, diff --git a/apps/readest-app/src/context/PHContext.tsx b/apps/readest-app/src/context/PHContext.tsx index cdd0331b..97222701 100644 --- a/apps/readest-app/src/context/PHContext.tsx +++ b/apps/readest-app/src/context/PHContext.tsx @@ -1,9 +1,10 @@ 'use client'; import posthog from 'posthog-js'; -import { ReactNode } from 'react'; +import { ReactNode, useEffect } from 'react'; import { PostHogProvider } from 'posthog-js/react'; import { TELEMETRY_OPT_OUT_KEY } from '@/utils/telemetry'; +import { getAppVersion } from '@/utils/version'; const shouldDisablePostHog = () => { if (typeof window === 'undefined') return false; @@ -27,5 +28,10 @@ if (typeof window !== 'undefined' && process.env['NODE_ENV'] === 'production' && } } export const CSPostHogProvider = ({ children }: { children: ReactNode }) => { + useEffect(() => { + posthog.register_for_session({ + $app_version: getAppVersion(), + }); + }, []); return {children}; }; diff --git a/apps/readest-app/src/helpers/updater.ts b/apps/readest-app/src/helpers/updater.ts index d1771943..e64cce1c 100644 --- a/apps/readest-app/src/helpers/updater.ts +++ b/apps/readest-app/src/helpers/updater.ts @@ -5,8 +5,8 @@ import { fetch } from '@tauri-apps/plugin-http'; import { WebviewWindow } from '@tauri-apps/api/webviewWindow'; import { TranslationFunc } from '@/hooks/useTranslation'; import { setUpdaterWindowVisible } from '@/components/UpdaterWindow'; +import { getAppVersion } from '@/utils/version'; import { CHECK_UPDATE_INTERVAL_SEC, READEST_UPDATER_FILE } from '@/services/constants'; -import packageJson from '../../package.json'; const LAST_CHECK_KEY = 'lastAppUpdateCheck'; @@ -49,7 +49,7 @@ export const checkForAppUpdates = async ( try { const response = await fetch(READEST_UPDATER_FILE, { connectTimeout: 5000 }); const data = await response.json(); - const isNewer = semver.gt(data.version, packageJson.version); + const isNewer = semver.gt(data.version, getAppVersion()); if (isNewer && ('android-arm64' in data.platforms || 'android-universal' in data.platforms)) { setUpdaterWindowVisible(true, data.version); } diff --git a/apps/readest-app/src/hooks/useQuotaStats.ts b/apps/readest-app/src/hooks/useQuotaStats.ts index 4d291175..3b37d3d7 100644 --- a/apps/readest-app/src/hooks/useQuotaStats.ts +++ b/apps/readest-app/src/hooks/useQuotaStats.ts @@ -16,8 +16,8 @@ export const useQuotaStats = () => { const userPlan = getUserPlan(token); const storagPlan = getStoragePlanData(token); const storageQuota: QuotaType = { - name: _('Storage'), - tooltip: _('{{percentage}}% of Cloud Storage Used.', { + name: _('Cloud Sync Storage'), + tooltip: _('{{percentage}}% of Cloud Sync Space Used.', { percentage: Math.round((storagPlan.usage / storagPlan.quota) * 100), }), used: Math.round(storagPlan.usage / 1024 / 1024), diff --git a/apps/readest-app/src/libs/sync.ts b/apps/readest-app/src/libs/sync.ts index dc39cd1a..139bdbf5 100644 --- a/apps/readest-app/src/libs/sync.ts +++ b/apps/readest-app/src/libs/sync.ts @@ -41,7 +41,7 @@ export class SyncClient { Authorization: `Bearer ${token}`, }, }, - 5000, + 8000, ); if (!res.ok) { @@ -70,7 +70,7 @@ export class SyncClient { }, body: JSON.stringify(payload), }, - 5000, + 8000, ); if (!res.ok) { diff --git a/apps/readest-app/src/utils/fetch.ts b/apps/readest-app/src/utils/fetch.ts index 94acb17a..f3547e7c 100644 --- a/apps/readest-app/src/utils/fetch.ts +++ b/apps/readest-app/src/utils/fetch.ts @@ -2,7 +2,7 @@ import { getAccessToken } from './access'; export const fetchWithTimeout = (url: string, options: RequestInit, timeout = 8000) => { const controller = new AbortController(); - const id = setTimeout(() => controller.abort(), timeout); + const id = setTimeout(() => controller.abort('Request timed out'), timeout); return fetch(url, { ...options, diff --git a/apps/readest-app/src/utils/version.ts b/apps/readest-app/src/utils/version.ts new file mode 100644 index 00000000..bbe12220 --- /dev/null +++ b/apps/readest-app/src/utils/version.ts @@ -0,0 +1,5 @@ +import packageJson from '../../package.json'; + +export const getAppVersion = () => { + return packageJson.version; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e785b2b6..75d57cef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,18 +53,21 @@ importers: '@opennextjs/cloudflare': specifier: ^1.3.1 version: 1.3.1(wrangler@4.21.0) + '@stripe/react-stripe-js': + specifier: ^3.7.0 + version: 3.7.0(@stripe/stripe-js@7.4.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@stripe/stripe-js': specifier: ^7.4.0 version: 7.4.0 '@supabase/auth-ui-react': specifier: ^0.4.7 - version: 0.4.7(@supabase/supabase-js@2.49.10) + version: 0.4.7(@supabase/supabase-js@2.50.2) '@supabase/auth-ui-shared': specifier: ^0.1.8 - version: 0.1.8(@supabase/supabase-js@2.49.10) + version: 0.1.8(@supabase/supabase-js@2.50.2) '@supabase/supabase-js': - specifier: ^2.49.10 - version: 2.49.10 + specifier: ^2.50.2 + version: 2.50.2 '@tauri-apps/api': specifier: 2.6.0 version: 2.6.0 @@ -2545,12 +2548,19 @@ packages: '@stitches/core@1.2.8': resolution: {integrity: sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg==} + '@stripe/react-stripe-js@3.7.0': + resolution: {integrity: sha512-PYls/2S9l0FF+2n0wHaEJsEU8x7CmBagiH7zYOsxbBlLIHEsqUIQ4MlIAbV9Zg6xwT8jlYdlRIyBTHmO3yM7kQ==} + peerDependencies: + '@stripe/stripe-js': '>=1.44.1 <8.0.0' + react: '>=16.8.0 <20.0.0' + react-dom: '>=16.8.0 <20.0.0' + '@stripe/stripe-js@7.4.0': resolution: {integrity: sha512-lQHQPfXPTBeh0XFjq6PqSBAyR7umwcJbvJhXV77uGCUDD6ymXJU/f2164ydLMLCCceNuPlbV9b+1smx98efwWQ==} engines: {node: '>=12.16'} - '@supabase/auth-js@2.69.1': - resolution: {integrity: sha512-FILtt5WjCNzmReeRLq5wRs3iShwmnWgBvxHfqapC/VoljJl+W8hDAyFmf1NVw3zH+ZjZ05AKxiKxVeb0HNWRMQ==} + '@supabase/auth-js@2.70.0': + resolution: {integrity: sha512-BaAK/tOAZFJtzF1sE3gJ2FwTjLf4ky3PSvcvLGEgEmO4BSBkwWKu8l67rLLIBZPDnCyV7Owk2uPyKHa0kj5QGg==} '@supabase/auth-ui-react@0.4.7': resolution: {integrity: sha512-Lp4FQGFh7BMX1Y/BFaUKidbryL7eskj1fl6Lby7BeHrTctbdvDbCMjVKS8wZ2rxuI8FtPS2iU900fSb70FHknQ==} @@ -2572,14 +2582,14 @@ packages: '@supabase/postgrest-js@1.19.4': resolution: {integrity: sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==} - '@supabase/realtime-js@2.11.10': - resolution: {integrity: sha512-SJKVa7EejnuyfImrbzx+HaD9i6T784khuw1zP+MBD7BmJYChegGxYigPzkKX8CK8nGuDntmeSD3fvriaH0EGZA==} + '@supabase/realtime-js@2.11.15': + resolution: {integrity: sha512-HQKRnwAqdVqJW/P9TjKVK+/ETpW4yQ8tyDPPtRMKOH4Uh3vQD74vmj353CYs8+YwVBKubeUOOEpI9CT8mT4obw==} '@supabase/storage-js@2.7.1': resolution: {integrity: sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==} - '@supabase/supabase-js@2.49.10': - resolution: {integrity: sha512-IRPcIdncuhD2m1eZ2Fkg0S1fq9SXlHfmAetBxPN66kVFtTucR8b01xKuVmKqcIJokB17umMf1bmqyS8yboXGsw==} + '@supabase/supabase-js@2.50.2': + resolution: {integrity: sha512-+27xlGgw7VyfwXXe+OiDJQosJNS+PPtjj1EnLR4uk+PKKZ91RA0/8NbIQybe6AGPanAaPtgOFFMMCArC6fZ++Q==} '@surma/rollup-plugin-off-main-thread@2.2.3': resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} @@ -4333,6 +4343,11 @@ packages: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} + isows@1.0.7: + resolution: {integrity: sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==} + peerDependencies: + ws: '*' + iterator.prototype@1.1.3: resolution: {integrity: sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ==} engines: {node: '>= 0.4'} @@ -9381,24 +9396,31 @@ snapshots: '@stitches/core@1.2.8': {} + '@stripe/react-stripe-js@3.7.0(@stripe/stripe-js@7.4.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@stripe/stripe-js': 7.4.0 + prop-types: 15.8.1 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + '@stripe/stripe-js@7.4.0': {} - '@supabase/auth-js@2.69.1': + '@supabase/auth-js@2.70.0': dependencies: '@supabase/node-fetch': 2.6.15 - '@supabase/auth-ui-react@0.4.7(@supabase/supabase-js@2.49.10)': + '@supabase/auth-ui-react@0.4.7(@supabase/supabase-js@2.50.2)': dependencies: '@stitches/core': 1.2.8 - '@supabase/auth-ui-shared': 0.1.8(@supabase/supabase-js@2.49.10) - '@supabase/supabase-js': 2.49.10 + '@supabase/auth-ui-shared': 0.1.8(@supabase/supabase-js@2.50.2) + '@supabase/supabase-js': 2.50.2 prop-types: 15.8.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@supabase/auth-ui-shared@0.1.8(@supabase/supabase-js@2.49.10)': + '@supabase/auth-ui-shared@0.1.8(@supabase/supabase-js@2.50.2)': dependencies: - '@supabase/supabase-js': 2.49.10 + '@supabase/supabase-js': 2.50.2 '@supabase/functions-js@2.4.4': dependencies: @@ -9412,11 +9434,12 @@ snapshots: dependencies: '@supabase/node-fetch': 2.6.15 - '@supabase/realtime-js@2.11.10': + '@supabase/realtime-js@2.11.15': dependencies: '@supabase/node-fetch': 2.6.15 '@types/phoenix': 1.6.6 '@types/ws': 8.18.1 + isows: 1.0.7(ws@8.18.2) ws: 8.18.2 transitivePeerDependencies: - bufferutil @@ -9426,13 +9449,13 @@ snapshots: dependencies: '@supabase/node-fetch': 2.6.15 - '@supabase/supabase-js@2.49.10': + '@supabase/supabase-js@2.50.2': dependencies: - '@supabase/auth-js': 2.69.1 + '@supabase/auth-js': 2.70.0 '@supabase/functions-js': 2.4.4 '@supabase/node-fetch': 2.6.15 '@supabase/postgrest-js': 1.19.4 - '@supabase/realtime-js': 2.11.10 + '@supabase/realtime-js': 2.11.15 '@supabase/storage-js': 2.7.1 transitivePeerDependencies: - bufferutil @@ -11410,6 +11433,10 @@ snapshots: isobject@3.0.1: {} + isows@1.0.7(ws@8.18.2): + dependencies: + ws: 8.18.2 + iterator.prototype@1.1.3: dependencies: define-properties: 1.2.1