feat: enable IAP for upgrading to Readest Premium on iOS (#1678)
This commit is contained in:
Generated
+102
-5
@@ -1507,6 +1507,15 @@ dependencies = [
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "file-id"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6bc904b9bbefcadbd8e3a9fb0d464a9b979de6324c03b3c663e8994f46a5be36"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.25"
|
||||
@@ -1608,6 +1617,15 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "funty"
|
||||
version = "2.0.0"
|
||||
@@ -2447,6 +2465,26 @@ dependencies = [
|
||||
"cfb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify-sys"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
@@ -2613,6 +2651,26 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
|
||||
dependencies = [
|
||||
"kqueue-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue-sys"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kuchikiki"
|
||||
version = "0.8.8-speedreader"
|
||||
@@ -2850,6 +2908,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
@@ -2948,6 +3007,46 @@ version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "8.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3163f59cd3fa0e9ef8c32f242966a7b9994fd7378366099593e0e73077cd8c97"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio",
|
||||
"notify-types",
|
||||
"walkdir",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-debouncer-full"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2d88b1a7538054351c8258338df7c931a590513fb3745e8c15eb9ff4199b8d1"
|
||||
dependencies = [
|
||||
"file-id",
|
||||
"log",
|
||||
"notify",
|
||||
"notify-types",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-types"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
@@ -5393,7 +5492,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-deep-link"
|
||||
version = "2.4.0"
|
||||
version = "2.4.1"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"rust-ini",
|
||||
@@ -5412,8 +5511,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-dialog"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05bedd4c3cf6f7aa97918a8814a736bd3695c9ddf3ede2d50eda6069c3290edc"
|
||||
dependencies = [
|
||||
"log",
|
||||
"raw-window-handle",
|
||||
@@ -5430,12 +5527,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-plugin-fs"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c6ef84ee2f2094ce093e55106d90d763ba343fad57566992962e8f76d113f99"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dunce",
|
||||
"glob",
|
||||
"notify",
|
||||
"notify-debouncer-full",
|
||||
"percent-encoding",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
|
||||
+8
-1
@@ -4,6 +4,8 @@ members = [
|
||||
"packages/tauri/crates/tauri",
|
||||
"packages/tauri/crates/tauri-utils",
|
||||
"packages/tauri/crates/tauri-build",
|
||||
"packages/tauri-plugins/plugins/fs",
|
||||
"packages/tauri-plugins/plugins/dialog",
|
||||
"packages/tauri-plugins/plugins/deep-link",
|
||||
]
|
||||
resolver = "2"
|
||||
@@ -16,8 +18,11 @@ tauri = { version = "2", default-features = false }
|
||||
tauri-build = "2"
|
||||
tauri-plugin = "2"
|
||||
tauri-utils = "2"
|
||||
schemars = "0.8"
|
||||
serde_json = "1"
|
||||
thiserror = "2"
|
||||
glob = "0.3"
|
||||
dunce = "1"
|
||||
url = "2"
|
||||
|
||||
[workspace.package]
|
||||
@@ -33,4 +38,6 @@ rust-version = "1.77.2"
|
||||
tauri = { path = "packages/tauri/crates/tauri" }
|
||||
tauri-utils = { path = "packages/tauri/crates/tauri-utils" }
|
||||
tauri-build = { path = "packages/tauri/crates/tauri-build" }
|
||||
tauri-plugin-deep-link = { path = "packages/tauri-plugins/plugins/deep-link" }
|
||||
tauri-plugin-fs = { path = "packages/tauri-plugins/plugins/fs" }
|
||||
tauri-plugin-dialog = { path = "packages/tauri-plugins/plugins/dialog" }
|
||||
tauri-plugin-deep-link = { path = "packages/tauri-plugins/plugins/deep-link" }
|
||||
|
||||
@@ -372,7 +372,6 @@
|
||||
"Upgrade to Plus or Pro": "الترقية إلى Plus أو Pro",
|
||||
"Current Plan": "الخطة الحالية",
|
||||
"Plan Limits": "حدود الخطة",
|
||||
"Failed to manage subscription. Please try again later.": "فشل في إدارة الاشتراك. يرجى المحاولة مرة أخرى لاحقًا.",
|
||||
"Processing your payment...": "جارٍ معالجة الدفع...",
|
||||
"Please wait while we confirm your subscription.": "يرجى الانتظار بينما نقوم بتأكيد اشتراكك.",
|
||||
"Payment Processing": "معالجة الدفع",
|
||||
@@ -469,5 +468,11 @@
|
||||
"Open Book in New Window": "فتح الكتاب في نافذة جديدة",
|
||||
"Voices for {{lang}}": "الأصوات لـ {{lang}}",
|
||||
"Yandex Translate": "ترجمة Yandex",
|
||||
"YYYY or YYYY-MM-DD": "YYYY أو YYYY-MM-DD"
|
||||
"YYYY or YYYY-MM-DD": "YYYY أو YYYY-MM-DD",
|
||||
"Restore Purchase": "استعادة الشراء",
|
||||
"No purchases found to restore.": "لا توجد مشتريات لاستعادتها.",
|
||||
"Failed to restore purchases.": "فشل في استعادة المشتريات.",
|
||||
"Failed to manage subscription.": "فشل في إدارة الاشتراك.",
|
||||
"Failed to load subscription plans.": "فشل في تحميل خطط الاشتراك.",
|
||||
"year": "سنة"
|
||||
}
|
||||
|
||||
@@ -356,7 +356,6 @@
|
||||
"Upgrade to Plus or Pro": "Upgrade auf Plus oder Pro",
|
||||
"Current Plan": "Aktueller Tarif",
|
||||
"Plan Limits": "Tariflimits",
|
||||
"Failed to manage subscription. Please try again later.": "Verwaltung des Abos fehlgeschlagen. Bitte versuchen Sie es später erneut.",
|
||||
"Processing your payment...": "Zahlung wird verarbeitet...",
|
||||
"Please wait while we confirm your subscription.": "Bitte warten Sie, während wir Ihr Abo bestätigen.",
|
||||
"Payment Processing": "Zahlung wird verarbeitet",
|
||||
@@ -453,5 +452,11 @@
|
||||
"Open Book in New Window": "Buch in neuem Fenster öffnen",
|
||||
"Voices for {{lang}}": "Stimmen für {{lang}}",
|
||||
"Yandex Translate": "Yandex Übersetzer",
|
||||
"YYYY or YYYY-MM-DD": "YYYY oder YYYY-MM-DD"
|
||||
"YYYY or YYYY-MM-DD": "YYYY oder YYYY-MM-DD",
|
||||
"Restore Purchase": "Kauf wiederherstellen",
|
||||
"No purchases found to restore.": "Keine Käufe zum Wiederherstellen gefunden.",
|
||||
"Failed to restore purchases.": "Wiederherstellung der Käufe fehlgeschlagen.",
|
||||
"Failed to manage subscription.": "Verwaltung des Abonnements fehlgeschlagen.",
|
||||
"Failed to load subscription plans.": "Laden der Abonnementpläne fehlgeschlagen.",
|
||||
"year": "Jahr"
|
||||
}
|
||||
|
||||
@@ -357,7 +357,6 @@
|
||||
"Upgrade to Plus or Pro": "Αναβάθμιση σε Plus ή Pro",
|
||||
"Current Plan": "Τρέχον πρόγραμμα",
|
||||
"Plan Limits": "Όρια προγράμματος",
|
||||
"Failed to manage subscription. Please try again later.": "Αποτυχία διαχείρισης της συνδρομής. Παρακαλώ δοκιμάστε ξανά αργότερα.",
|
||||
"Processing your payment...": "Επεξεργασία πληρωμής...",
|
||||
"Please wait while we confirm your subscription.": "Παρακαλώ περιμένετε όσο επιβεβαιώνουμε τη συνδρομή σας.",
|
||||
"Payment Processing": "Επεξεργασία πληρωμής",
|
||||
@@ -453,5 +452,11 @@
|
||||
"Open Book in New Window": "Άνοιγμα βιβλίου σε νέο παράθυρο",
|
||||
"Voices for {{lang}}": "Φωνές για {{lang}}",
|
||||
"Yandex Translate": "Μετάφραση Yandex",
|
||||
"YYYY or YYYY-MM-DD": "YYYY ή YYYY-MM-DD"
|
||||
"YYYY or YYYY-MM-DD": "YYYY ή YYYY-MM-DD",
|
||||
"Restore Purchase": "Επαναφορά αγοράς",
|
||||
"No purchases found to restore.": "Δεν βρέθηκαν αγορές για επαναφορά.",
|
||||
"Failed to restore purchases.": "Αποτυχία επαναφοράς αγορών.",
|
||||
"Failed to manage subscription.": "Αποτυχία διαχείρισης συνδρομής.",
|
||||
"Failed to load subscription plans.": "Αποτυχία φόρτωσης σχεδίων συνδρομής.",
|
||||
"year": "έτος"
|
||||
}
|
||||
|
||||
@@ -361,7 +361,6 @@
|
||||
"Upgrade to Plus or Pro": "Actualizar a Plus o Pro",
|
||||
"Current Plan": "Plan actual",
|
||||
"Plan Limits": "Límites del plan",
|
||||
"Failed to manage subscription. Please try again later.": "No se pudo gestionar la suscripción. Por favor, inténtalo de nuevo más tarde.",
|
||||
"Processing your payment...": "Procesando tu pago...",
|
||||
"Please wait while we confirm your subscription.": "Espera mientras confirmamos tu suscripción.",
|
||||
"Payment Processing": "Procesando pago",
|
||||
@@ -457,5 +456,11 @@
|
||||
"Open Book in New Window": "Abre el libro en una nueva ventana",
|
||||
"Voices for {{lang}}": "Voces para {{lang}}",
|
||||
"Yandex Translate": "Yandex Translate",
|
||||
"YYYY or YYYY-MM-DD": "YYYY o YYYY-MM-DD"
|
||||
"YYYY or YYYY-MM-DD": "YYYY o YYYY-MM-DD",
|
||||
"Restore Purchase": "Restaurar compra",
|
||||
"No purchases found to restore.": "No se encontraron compras para restaurar.",
|
||||
"Failed to restore purchases.": "Error al restaurar compras.",
|
||||
"Failed to manage subscription.": "Error al gestionar la suscripción.",
|
||||
"Failed to load subscription plans.": "Error al cargar los planes de suscripción.",
|
||||
"year": "año"
|
||||
}
|
||||
|
||||
@@ -360,7 +360,6 @@
|
||||
"Upgrade to Plus or Pro": "Passer à Plus ou Pro",
|
||||
"Current Plan": "Abonnement actuel",
|
||||
"Plan Limits": "Limites de l’abonnement",
|
||||
"Failed to manage subscription. Please try again later.": "Échec de la gestion de l’abonnement. Veuillez réessayer plus tard.",
|
||||
"Processing your payment...": "Traitement de votre paiement...",
|
||||
"Please wait while we confirm your subscription.": "Veuillez patienter pendant la confirmation de votre abonnement.",
|
||||
"Payment Processing": "Traitement du paiement",
|
||||
@@ -457,5 +456,11 @@
|
||||
"Open Book in New Window": "Ouvrir dans une nouvelle fenêtre",
|
||||
"Voices for {{lang}}": "Voix pour {{lang}}",
|
||||
"Yandex Translate": "Yandex Traduction",
|
||||
"YYYY or YYYY-MM-DD": "YYYY ou YYYY-MM-DD"
|
||||
"YYYY or YYYY-MM-DD": "YYYY ou YYYY-MM-DD",
|
||||
"Restore Purchase": "Restaurer l'achat",
|
||||
"No purchases found to restore.": "Aucun achat trouvé à restaurer.",
|
||||
"Failed to restore purchases.": "Échec de la restauration des achats.",
|
||||
"Failed to manage subscription.": "Échec de la gestion de l'abonnement.",
|
||||
"Failed to load subscription plans.": "Échec du chargement des plans d'abonnement.",
|
||||
"year": "année"
|
||||
}
|
||||
|
||||
@@ -357,7 +357,6 @@
|
||||
"Upgrade to Plus or Pro": "Plus या Pro में अपग्रेड करें",
|
||||
"Current Plan": "वर्तमान योजना",
|
||||
"Plan Limits": "योजना की सीमाएँ",
|
||||
"Failed to manage subscription. Please try again later.": "सदस्यता प्रबंधित करने में विफल। कृपया बाद में पुनः प्रयास करें।",
|
||||
"Processing your payment...": "आपका भुगतान संसाधित हो रहा है...",
|
||||
"Please wait while we confirm your subscription.": "कृपया प्रतीक्षा करें, हम आपकी सदस्यता की पुष्टि कर रहे हैं।",
|
||||
"Payment Processing": "भुगतान संसाधित हो रहा है",
|
||||
@@ -453,5 +452,11 @@
|
||||
"Open Book in New Window": "नई विंडो में पुस्तक खोलें",
|
||||
"Voices for {{lang}}": "{{lang}} के लिए आवाज़ें",
|
||||
"Yandex Translate": "यांडेक्स अनुवाद",
|
||||
"YYYY or YYYY-MM-DD": "YYYY या YYYY-MM-DD"
|
||||
"YYYY or YYYY-MM-DD": "YYYY या YYYY-MM-DD",
|
||||
"Restore Purchase": "खरीदारी पुनर्स्थापित करें",
|
||||
"No purchases found to restore.": "पुनर्स्थापित करने के लिए कोई खरीदारी नहीं मिली।",
|
||||
"Failed to restore purchases.": "खरीदारी पुनर्स्थापित करने में विफल।",
|
||||
"Failed to manage subscription.": "सदस्यता प्रबंधित करने में विफल।",
|
||||
"Failed to load subscription plans.": "सदस्यता योजनाएँ लोड करने में विफल।",
|
||||
"year": "वर्ष"
|
||||
}
|
||||
|
||||
@@ -353,7 +353,6 @@
|
||||
"Upgrade to Plus or Pro": "Tingkatkan ke Plus atau Pro",
|
||||
"Current Plan": "Paket Saat Ini",
|
||||
"Plan Limits": "Batas Paket",
|
||||
"Failed to manage subscription. Please try again later.": "Gagal mengelola langganan. Silakan coba lagi nanti.",
|
||||
"Processing your payment...": "Memproses pembayaran Anda...",
|
||||
"Please wait while we confirm your subscription.": "Mohon tunggu saat kami mengonfirmasi langganan Anda.",
|
||||
"Payment Processing": "Memproses Pembayaran",
|
||||
@@ -449,5 +448,11 @@
|
||||
"Open Book in New Window": "Buka Buku di Jendela Baru",
|
||||
"Voices for {{lang}}": "Suara untuk {{lang}}",
|
||||
"Yandex Translate": "Yandex Translate",
|
||||
"YYYY or YYYY-MM-DD": "YYYY atau YYYY-MM-DD"
|
||||
"YYYY or YYYY-MM-DD": "YYYY atau YYYY-MM-DD",
|
||||
"Restore Purchase": "Pulihkan Pembelian",
|
||||
"No purchases found to restore.": "Tidak ada pembelian yang ditemukan untuk dipulihkan.",
|
||||
"Failed to restore purchases.": "Gagal memulihkan pembelian.",
|
||||
"Failed to manage subscription.": "Gagal mengelola langganan.",
|
||||
"Failed to load subscription plans.": "Gagal memuat rencana langganan.",
|
||||
"year": "tahun"
|
||||
}
|
||||
|
||||
@@ -361,7 +361,6 @@
|
||||
"Upgrade to Plus or Pro": "Aggiorna a Plus o Pro",
|
||||
"Current Plan": "Piano Attuale",
|
||||
"Plan Limits": "Limiti del Piano",
|
||||
"Failed to manage subscription. Please try again later.": "Impossibile gestire l'abbonamento. Riprova più tardi.",
|
||||
"Processing your payment...": "Elaborazione del pagamento...",
|
||||
"Please wait while we confirm your subscription.": "Attendi mentre confermiamo il tuo abbonamento.",
|
||||
"Payment Processing": "Elaborazione Pagamento",
|
||||
@@ -457,5 +456,11 @@
|
||||
"Open Book in New Window": "Apri libro in una nuova finestra",
|
||||
"Voices for {{lang}}": "Voci per {{lang}}",
|
||||
"Yandex Translate": "Yandex Translate",
|
||||
"YYYY or YYYY-MM-DD": "AAAA o AAAA-MM-GG"
|
||||
"YYYY or YYYY-MM-DD": "AAAA o AAAA-MM-GG",
|
||||
"Restore Purchase": "Ripristina acquisto",
|
||||
"No purchases found to restore.": "Nessun acquisto trovato da ripristinare.",
|
||||
"Failed to restore purchases.": "Impossibile ripristinare gli acquisti.",
|
||||
"Failed to manage subscription.": "Impossibile gestire l'abbonamento.",
|
||||
"Failed to load subscription plans.": "Impossibile caricare i piani di abbonamento.",
|
||||
"year": "anno"
|
||||
}
|
||||
|
||||
@@ -353,7 +353,6 @@
|
||||
"Upgrade to Plus or Pro": "Plus または Pro にアップグレード",
|
||||
"Current Plan": "現在のプラン",
|
||||
"Plan Limits": "プランの上限",
|
||||
"Failed to manage subscription. Please try again later.": "サブスクリプションの管理に失敗しました。後でもう一度お試しください。",
|
||||
"Processing your payment...": "お支払いを処理中...",
|
||||
"Please wait while we confirm your subscription.": "サブスクリプションを確認しています。しばらくお待ちください。",
|
||||
"Payment Processing": "支払いを処理中",
|
||||
@@ -449,5 +448,11 @@
|
||||
"Open Book in New Window": "新しいウィンドウで本を開く",
|
||||
"Voices for {{lang}}": "{{lang}}の音声",
|
||||
"Yandex Translate": "Yandex翻訳",
|
||||
"YYYY or YYYY-MM-DD": "YYYYまたはYYYY-MM-DD"
|
||||
"YYYY or YYYY-MM-DD": "YYYYまたはYYYY-MM-DD",
|
||||
"Restore Purchase": "購入を復元",
|
||||
"No purchases found to restore.": "復元する購入が見つかりません。",
|
||||
"Failed to restore purchases.": "購入の復元に失敗しました。",
|
||||
"Failed to manage subscription.": "サブスクリプションの管理に失敗しました。",
|
||||
"Failed to load subscription plans.": "サブスクリプションプランの読み込みに失敗しました。",
|
||||
"year": "年"
|
||||
}
|
||||
|
||||
@@ -353,7 +353,6 @@
|
||||
"Upgrade to Plus or Pro": "Plus 또는 Pro로 업그레이드",
|
||||
"Current Plan": "현재 요금제",
|
||||
"Plan Limits": "요금제 한도",
|
||||
"Failed to manage subscription. Please try again later.": "구독 관리에 실패했습니다. 나중에 다시 시도해주세요.",
|
||||
"Processing your payment...": "결제를 처리 중입니다...",
|
||||
"Please wait while we confirm your subscription.": "구독을 확인하는 중입니다. 잠시만 기다려주세요.",
|
||||
"Payment Processing": "결제 처리 중",
|
||||
@@ -449,5 +448,11 @@
|
||||
"Open Book in New Window": "새 창에서 책 열기",
|
||||
"Voices for {{lang}}": "{{lang}}의 음성",
|
||||
"Yandex Translate": "Yandex 번역",
|
||||
"YYYY or YYYY-MM-DD": "YYYY 또는 YYYY-MM-DD"
|
||||
"YYYY or YYYY-MM-DD": "YYYY 또는 YYYY-MM-DD",
|
||||
"Restore Purchase": "구매 복원",
|
||||
"No purchases found to restore.": "복원할 구매가 없습니다.",
|
||||
"Failed to restore purchases.": "구매 복원에 실패했습니다.",
|
||||
"Failed to manage subscription.": "구독 관리에 실패했습니다.",
|
||||
"Failed to load subscription plans.": "구독 플랜 로드에 실패했습니다.",
|
||||
"year": "년"
|
||||
}
|
||||
|
||||
@@ -357,7 +357,6 @@
|
||||
"Upgrade to Plus or Pro": "Upgrade naar Plus of Pro",
|
||||
"Current Plan": "Huidig abonnement",
|
||||
"Plan Limits": "Abonnementslimieten",
|
||||
"Failed to manage subscription. Please try again later.": "Abonnement beheren is mislukt. Probeer het later opnieuw.",
|
||||
"Processing your payment...": "Je betaling wordt verwerkt...",
|
||||
"Please wait while we confirm your subscription.": "Even geduld terwijl we je abonnement bevestigen.",
|
||||
"Payment Processing": "Betaling verwerken",
|
||||
@@ -453,5 +452,11 @@
|
||||
"Open Book in New Window": "Open boek in nieuw venster",
|
||||
"Voices for {{lang}}": "Stemmen voor {{lang}}",
|
||||
"Yandex Translate": "Yandex Vertalen",
|
||||
"YYYY or YYYY-MM-DD": "YYYY of YYYY-MM-DD"
|
||||
"YYYY or YYYY-MM-DD": "YYYY of YYYY-MM-DD",
|
||||
"Restore Purchase": "Herstel aankoop",
|
||||
"No purchases found to restore.": "Geen aankopen gevonden om te herstellen.",
|
||||
"Failed to restore purchases.": "Herstellen van aankopen is mislukt.",
|
||||
"Failed to manage subscription.": "Beheren van abonnement is mislukt.",
|
||||
"Failed to load subscription plans.": "Laden van abonnement plannen is mislukt.",
|
||||
"year": "jaar"
|
||||
}
|
||||
|
||||
@@ -365,7 +365,6 @@
|
||||
"Upgrade to Plus or Pro": "Uaktualnij do Plus lub Pro",
|
||||
"Current Plan": "Obecny plan",
|
||||
"Plan Limits": "Limity planu",
|
||||
"Failed to manage subscription. Please try again later.": "Nie udało się zarządzać subskrypcją. Spróbuj ponownie później.",
|
||||
"Processing your payment...": "Przetwarzanie płatności...",
|
||||
"Please wait while we confirm your subscription.": "Proszę czekać, potwierdzamy subskrypcję.",
|
||||
"Payment Processing": "Przetwarzanie płatności",
|
||||
@@ -461,5 +460,11 @@
|
||||
"Open Book in New Window": "Otwórz książkę w nowym oknie",
|
||||
"Voices for {{lang}}": "{{lang}} Głosy",
|
||||
"Yandex Translate": "Yandex Tłumacz",
|
||||
"YYYY or YYYY-MM-DD": "YYYY lub YYYY-MM-DD"
|
||||
"YYYY or YYYY-MM-DD": "YYYY lub YYYY-MM-DD",
|
||||
"Restore Purchase": "Przywróć zakup",
|
||||
"No purchases found to restore.": "Nie znaleziono zakupów do przywrócenia.",
|
||||
"Failed to restore purchases.": "Nie udało się przywrócić zakupów.",
|
||||
"Failed to manage subscription.": "Nie udało się zarządzać subskrypcją.",
|
||||
"Failed to load subscription plans.": "Nie udało się załadować planów subskrypcyjnych.",
|
||||
"year": "rok"
|
||||
}
|
||||
|
||||
@@ -361,7 +361,6 @@
|
||||
"Upgrade to Plus or Pro": "Atualizar para Plus ou Pro",
|
||||
"Current Plan": "Plano atual",
|
||||
"Plan Limits": "Limites do plano",
|
||||
"Failed to manage subscription. Please try again later.": "Não foi possível gerenciar a assinatura. Tente novamente mais tarde.",
|
||||
"Processing your payment...": "Processando seu pagamento...",
|
||||
"Please wait while we confirm your subscription.": "Por favor, aguarde enquanto confirmamos sua assinatura.",
|
||||
"Payment Processing": "Processando pagamento",
|
||||
@@ -457,5 +456,11 @@
|
||||
"Open Book in New Window": "Abrir livro em nova janela",
|
||||
"Voices for {{lang}}": "Vozes para {{lang}}",
|
||||
"Yandex Translate": "Yandex Translate",
|
||||
"YYYY or YYYY-MM-DD": "YYYY ou YYYY-MM-DD"
|
||||
"YYYY or YYYY-MM-DD": "YYYY ou YYYY-MM-DD",
|
||||
"Restore Purchase": "Restaurar Compra",
|
||||
"No purchases found to restore.": "Nenhuma compra encontrada para restaurar.",
|
||||
"Failed to restore purchases.": "Falha ao restaurar compras.",
|
||||
"Failed to manage subscription.": "Falha ao gerenciar assinatura.",
|
||||
"Failed to load subscription plans.": "Falha ao carregar planos de assinatura.",
|
||||
"year": "ano"
|
||||
}
|
||||
|
||||
@@ -365,7 +365,6 @@
|
||||
"Upgrade to Plus or Pro": "Обновить до Plus или Pro",
|
||||
"Current Plan": "Текущий план",
|
||||
"Plan Limits": "Лимиты плана",
|
||||
"Failed to manage subscription. Please try again later.": "Не удалось управлять подпиской. Пожалуйста, попробуйте позже.",
|
||||
"Processing your payment...": "Обработка платежа...",
|
||||
"Please wait while we confirm your subscription.": "Пожалуйста, подождите, пока мы подтверждаем вашу подписку.",
|
||||
"Payment Processing": "Обработка платежа",
|
||||
@@ -461,5 +460,11 @@
|
||||
"Open Book in New Window": "Открыть книгу в новом окне",
|
||||
"Voices for {{lang}}": "Голоса для {{lang}}",
|
||||
"Yandex Translate": "Yandex Translate",
|
||||
"YYYY or YYYY-MM-DD": "YYYY или YYYY-MM-DD"
|
||||
"YYYY or YYYY-MM-DD": "YYYY или YYYY-MM-DD",
|
||||
"Restore Purchase": "Восстановить покупку",
|
||||
"No purchases found to restore.": "Не найдено покупок для восстановления.",
|
||||
"Failed to restore purchases.": "Не удалось восстановить покупки.",
|
||||
"Failed to manage subscription.": "Не удалось управлять подпиской.",
|
||||
"Failed to load subscription plans.": "Не удалось загрузить планы подписки.",
|
||||
"year": "год"
|
||||
}
|
||||
|
||||
@@ -306,7 +306,6 @@
|
||||
"Current Plan": "แผนปัจจุบัน",
|
||||
"Plan Limits": "ข้อจำกัดแผน",
|
||||
"Failed to delete user. Please try again later.": "ไม่สามารถลบผู้ใช้ ลองใหม่ภายหลัง",
|
||||
"Failed to manage subscription. Please try again later.": "ไม่สามารถจัดการสมาชิก ลองใหม่ภายหลัง",
|
||||
"Loading profile...": "กำลังโหลดโปรไฟล์...",
|
||||
"Processing your payment...": "กำลังประมวลผลการชำระเงิน...",
|
||||
"Please wait while we confirm your subscription.": "กรุณารอสักครู่เพื่อยืนยันการสมัครสมาชิก",
|
||||
@@ -449,5 +448,11 @@
|
||||
"Open Book in New Window": "เปิดหนังสือในหน้าต่างใหม่",
|
||||
"Voices for {{lang}}": "เสียงสำหรับ {{lang}}",
|
||||
"Yandex Translate": "Yandex Translate",
|
||||
"YYYY or YYYY-MM-DD": "YYYY หรือ YYYY-MM-DD"
|
||||
"YYYY or YYYY-MM-DD": "YYYY หรือ YYYY-MM-DD",
|
||||
"Restore Purchase": "กู้คืนการซื้อ",
|
||||
"No purchases found to restore.": "ไม่พบการซื้อเพื่อกู้คืน",
|
||||
"Failed to restore purchases.": "ไม่สามารถกู้คืนการซื้อได้",
|
||||
"Failed to manage subscription.": "ไม่สามารถจัดการการสมัครสมาชิกได้",
|
||||
"Failed to load subscription plans.": "ไม่สามารถโหลดแผนการสมัครสมาชิกได้",
|
||||
"year": "ปี"
|
||||
}
|
||||
|
||||
@@ -357,7 +357,6 @@
|
||||
"Upgrade to Plus or Pro": "Plus veya Pro'ya yükselt",
|
||||
"Current Plan": "Mevcut Plan",
|
||||
"Plan Limits": "Plan Limitleri",
|
||||
"Failed to manage subscription. Please try again later.": "Abonelik yönetimi başarısız oldu. Lütfen daha sonra tekrar deneyin.",
|
||||
"Processing your payment...": "Ödemeniz işleniyor...",
|
||||
"Please wait while we confirm your subscription.": "Aboneliğinizi onaylıyoruz, lütfen bekleyin.",
|
||||
"Payment Processing": "Ödeme İşleniyor",
|
||||
@@ -453,5 +452,11 @@
|
||||
"Open Book in New Window": "Kitabı Yeni Pencerede Aç",
|
||||
"Voices for {{lang}}": "{{lang}} için Sesler",
|
||||
"Yandex Translate": "Yandex Çeviri",
|
||||
"YYYY or YYYY-MM-DD": "YYYY veya YYYY-MM-DD"
|
||||
"YYYY or YYYY-MM-DD": "YYYY veya YYYY-MM-DD",
|
||||
"Restore Purchase": "Satın Almayı Geri Yükle",
|
||||
"No purchases found to restore.": "Geri yüklemek için satın alma bulunamadı.",
|
||||
"Failed to restore purchases.": "Satın alımları geri yükleme başarısız oldu.",
|
||||
"Failed to manage subscription.": "Aboneliği yönetme başarısız oldu.",
|
||||
"Failed to load subscription plans.": "Abonelik planlarını yükleme başarısız oldu.",
|
||||
"year": "yıl"
|
||||
}
|
||||
|
||||
@@ -365,7 +365,6 @@
|
||||
"Upgrade to Plus or Pro": "Перейти на Plus або Pro",
|
||||
"Current Plan": "Поточний план",
|
||||
"Plan Limits": "Обмеження плану",
|
||||
"Failed to manage subscription. Please try again later.": "Не вдалося керувати підпискою. Спробуйте ще раз пізніше.",
|
||||
"Processing your payment...": "Обробка вашого платежу...",
|
||||
"Please wait while we confirm your subscription.": "Зачекайте, ми підтверджуємо вашу підписку.",
|
||||
"Payment Processing": "Обробка платежу",
|
||||
@@ -461,5 +460,11 @@
|
||||
"Open Book in New Window": "Відкрити книгу в новому вікні",
|
||||
"Voices for {{lang}}": "Голоси для {{lang}}",
|
||||
"Yandex Translate": "Yandex Перекладач",
|
||||
"YYYY or YYYY-MM-DD": "YYYY або YYYY-MM-DD"
|
||||
"YYYY or YYYY-MM-DD": "YYYY або YYYY-MM-DD",
|
||||
"Restore Purchase": "Відновити покупку",
|
||||
"No purchases found to restore.": "Не знайдено покупок для відновлення.",
|
||||
"Failed to restore purchases.": "Не вдалося відновити покупки.",
|
||||
"Failed to manage subscription.": "Не вдалося керувати підпискою.",
|
||||
"Failed to load subscription plans.": "Не вдалося завантажити плани підписки.",
|
||||
"year": "рік"
|
||||
}
|
||||
|
||||
@@ -353,7 +353,6 @@
|
||||
"Upgrade to Plus or Pro": "Nâng cấp lên Plus hoặc Pro",
|
||||
"Current Plan": "Gói hiện tại",
|
||||
"Plan Limits": "Giới hạn gói",
|
||||
"Failed to manage subscription. Please try again later.": "Không thể quản lý đăng ký. Vui lòng thử lại sau.",
|
||||
"Processing your payment...": "Đang xử lý thanh toán...",
|
||||
"Please wait while we confirm your subscription.": "Vui lòng chờ trong khi chúng tôi xác nhận đăng ký của bạn.",
|
||||
"Payment Processing": "Đang xử lý thanh toán",
|
||||
@@ -449,5 +448,11 @@
|
||||
"Open Book in New Window": "Mở Sách trong Cửa sổ Mới",
|
||||
"Voices for {{lang}}": "Giọng nói cho {{lang}}",
|
||||
"Yandex Translate": "Yandex Dịch",
|
||||
"YYYY or YYYY-MM-DD": "YYYY hoặc YYYY-MM-DD"
|
||||
"YYYY or YYYY-MM-DD": "YYYY hoặc YYYY-MM-DD",
|
||||
"Restore Purchase": "Khôi phục mua hàng",
|
||||
"No purchases found to restore.": "Không tìm thấy giao dịch mua nào để khôi phục.",
|
||||
"Failed to restore purchases.": "Khôi phục giao dịch mua không thành công.",
|
||||
"Failed to manage subscription.": "Quản lý đăng ký không thành công.",
|
||||
"Failed to load subscription plans.": "Tải kế hoạch đăng ký không thành công.",
|
||||
"year": "năm"
|
||||
}
|
||||
|
||||
@@ -353,7 +353,6 @@
|
||||
"Upgrade to Plus or Pro": "升级到 Plus 或 Pro",
|
||||
"Current Plan": "当前套餐",
|
||||
"Plan Limits": "套餐限制",
|
||||
"Failed to manage subscription. Please try again later.": "无法管理订阅,请稍后再试。",
|
||||
"Processing your payment...": "正在处理付款...",
|
||||
"Please wait while we confirm your subscription.": "请稍候,我们正在确认您的订阅。",
|
||||
"Payment Processing": "付款处理中",
|
||||
@@ -449,5 +448,11 @@
|
||||
"Open Book in New Window": "在新窗口中打开书籍",
|
||||
"Voices for {{lang}}": "{{lang}} 的语音",
|
||||
"Yandex Translate": "Yandex 翻译",
|
||||
"YYYY or YYYY-MM-DD": "YYYY 或 YYYY-MM-DD"
|
||||
"YYYY or YYYY-MM-DD": "YYYY 或 YYYY-MM-DD",
|
||||
"Restore Purchase": "恢复购买",
|
||||
"No purchases found to restore.": "未找到可恢复的购买。",
|
||||
"Failed to restore purchases.": "恢复购买失败。",
|
||||
"Failed to manage subscription.": "管理订阅失败。",
|
||||
"Failed to load subscription plans.": "加载订阅计划失败。",
|
||||
"year": "年"
|
||||
}
|
||||
|
||||
@@ -353,7 +353,6 @@
|
||||
"Upgrade to Plus or Pro": "升級到 Plus 或 Pro",
|
||||
"Current Plan": "當前方案",
|
||||
"Plan Limits": "方案限制",
|
||||
"Failed to manage subscription. Please try again later.": "無法管理訂閱,請稍後再試。",
|
||||
"Processing your payment...": "正在處理付款...",
|
||||
"Please wait while we confirm your subscription.": "請稍候,我們正在確認您的訂閱。",
|
||||
"Payment Processing": "付款處理中",
|
||||
@@ -449,5 +448,11 @@
|
||||
"Open Book in New Window": "在新視窗中打開書籍",
|
||||
"Voices for {{lang}}": "{{lang}} 的語音",
|
||||
"Yandex Translate": "Yandex 翻譯",
|
||||
"YYYY or YYYY-MM-DD": "YYYY 或 YYYY-MM-DD"
|
||||
"YYYY or YYYY-MM-DD": "YYYY 或 YYYY-MM-DD",
|
||||
"Restore Purchase": "恢復購買",
|
||||
"No purchases found to restore.": "未找到可恢復的購買。",
|
||||
"Failed to restore purchases.": "恢復購買失敗。",
|
||||
"Failed to manage subscription.": "管理訂閱失敗。",
|
||||
"Failed to load subscription plans.": "加載訂閱計劃失敗。",
|
||||
"year": "年"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { NextResponse } from 'next/server';
|
||||
import { defaultIAPVerifier } from '@/libs/iap/apple/verifier';
|
||||
import { createSupabaseAdminClient } from '@/utils/supabase';
|
||||
import { validateUserAndToken } from '@/utils/access';
|
||||
import { IAPError } from '@/types/error';
|
||||
|
||||
const iapVerificationSchema = z.object({
|
||||
transactionId: z.string().min(1, 'Transaction ID is required'),
|
||||
@@ -10,10 +11,10 @@ const iapVerificationSchema = z.object({
|
||||
});
|
||||
|
||||
const PRODUCT_MAP: Record<string, string> = {
|
||||
'com.bilingify.readest.monthly.plus': 'Monthly Plus Subscription',
|
||||
'com.bilingify.readest.yearly.plus': 'Yearly Plus Subscription',
|
||||
'com.bilingify.readest.monthly.pro': 'Monthly Pro Subscription',
|
||||
'com.bilingify.readest.yearly.pro': 'Yearly Pro Subscription',
|
||||
'com.bilingify.readest.monthly.plus': 'Plus',
|
||||
'com.bilingify.readest.yearly.plus': 'Plus',
|
||||
'com.bilingify.readest.monthly.pro': 'Pro',
|
||||
'com.bilingify.readest.yearly.pro': 'Pro',
|
||||
};
|
||||
|
||||
const getProductName = (productId: string) => {
|
||||
@@ -118,7 +119,7 @@ export async function POST(request: Request) {
|
||||
|
||||
const { user, token } = await validateUserAndToken(request.headers.get('authorization'));
|
||||
if (!user || !token) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 403 });
|
||||
return NextResponse.json({ error: IAPError.NOT_AUTHENTICATED }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -126,12 +127,22 @@ export async function POST(request: Request) {
|
||||
const { data: existingSubscription } = await supabase
|
||||
.from('apple_iap_subscriptions')
|
||||
.select('*')
|
||||
.eq('transaction_id', transactionId)
|
||||
.eq('user_id', user.id)
|
||||
.eq('original_transaction_id', originalTransactionId)
|
||||
.single();
|
||||
|
||||
console.log('Existing subscription:', existingSubscription);
|
||||
if (existingSubscription && existingSubscription.status === 'active') {
|
||||
// Should not restore purchase for another account
|
||||
if (existingSubscription && existingSubscription.user_id !== user.id) {
|
||||
return NextResponse.json(
|
||||
{ error: IAPError.TRANSACTION_BELONGS_TO_ANOTHER_USER },
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
if (
|
||||
existingSubscription &&
|
||||
existingSubscription.transactionId === transactionId &&
|
||||
existingSubscription.status === 'active'
|
||||
) {
|
||||
console.log('Transaction already verified and active');
|
||||
|
||||
const purchase = {
|
||||
@@ -140,7 +151,7 @@ export async function POST(request: Request) {
|
||||
subscriptionId:
|
||||
existingSubscription.web_order_line_item_id ||
|
||||
existingSubscription.original_transaction_id,
|
||||
planName: await getProductName(existingSubscription.product_id),
|
||||
planName: getProductName(existingSubscription.product_id),
|
||||
productId: existingSubscription.product_id,
|
||||
platform: existingSubscription.platform,
|
||||
transactionId: existingSubscription.transaction_id,
|
||||
@@ -163,7 +174,7 @@ export async function POST(request: Request) {
|
||||
console.error('Apple verification failed:', verificationResult.error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: verificationResult.error || 'Transaction verification failed with Apple',
|
||||
error: verificationResult.error || IAPError.UNKNOWN_ERROR,
|
||||
purchase: null,
|
||||
},
|
||||
{ status: 400 },
|
||||
@@ -184,7 +195,7 @@ export async function POST(request: Request) {
|
||||
status: verificationResult.status!,
|
||||
customerEmail: user.email!,
|
||||
subscriptionId: transaction.webOrderLineItemId || transaction.originalTransactionId,
|
||||
planName: await getProductName(transaction.productId),
|
||||
planName: getProductName(transaction.productId),
|
||||
productId: transaction.productId,
|
||||
platform: 'ios',
|
||||
transactionId: transaction.transactionId,
|
||||
@@ -208,7 +219,7 @@ export async function POST(request: Request) {
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Transaction failed to update database. Please contact support.',
|
||||
error: IAPError.TRANSACTION_SERVICE_UNAVAILABLE,
|
||||
purchase: null,
|
||||
},
|
||||
{ status: 500 },
|
||||
@@ -222,7 +233,7 @@ export async function POST(request: Request) {
|
||||
} catch (error) {
|
||||
console.error('IAP verification error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : 'Unknown error' },
|
||||
{ error: error instanceof Error ? error.message : IAPError.UNKNOWN_ERROR },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -436,7 +436,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
};
|
||||
|
||||
const selectFilesTauri = async () => {
|
||||
const exts = appService?.isMobileApp ? [] : SUPPORTED_FILE_EXTS;
|
||||
const exts = appService?.isIOSApp ? [] : SUPPORTED_FILE_EXTS;
|
||||
const files = (await appService?.selectFiles(_('Select Books'), exts)) || [];
|
||||
if (appService?.isIOSApp) {
|
||||
return files.filter((file) => {
|
||||
@@ -444,7 +444,6 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
return SUPPORTED_FILE_EXTS.includes(fileExt);
|
||||
});
|
||||
}
|
||||
// Cannot filter out files on Android since some content providers may not return the file name
|
||||
return files;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { UserPlan } from '@/types/user';
|
||||
|
||||
@@ -48,16 +49,19 @@ interface AccountActionsProps {
|
||||
userPlan: UserPlan;
|
||||
onLogout: () => void;
|
||||
onConfirmDelete: () => void;
|
||||
onManageSubscription: () => void;
|
||||
onRestorePurchase?: () => void;
|
||||
onManageSubscription?: () => void;
|
||||
}
|
||||
|
||||
const AccountActions: React.FC<AccountActionsProps> = ({
|
||||
userPlan,
|
||||
onLogout,
|
||||
onConfirmDelete,
|
||||
onRestorePurchase,
|
||||
onManageSubscription,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false);
|
||||
|
||||
const handleDeleteRequest = () => {
|
||||
@@ -79,13 +83,22 @@ const AccountActions: React.FC<AccountActionsProps> = ({
|
||||
}}
|
||||
/>
|
||||
<div className='flex flex-col gap-4 md:flex-row'>
|
||||
{userPlan !== 'free' && (
|
||||
{appService?.isIOSApp ? (
|
||||
<button
|
||||
onClick={onManageSubscription}
|
||||
onClick={onRestorePurchase}
|
||||
className='w-full rounded-lg bg-blue-100 px-6 py-3 font-medium text-blue-600 transition-colors hover:bg-blue-200 md:w-auto'
|
||||
>
|
||||
{_('Manage Subscription')}
|
||||
{_('Restore Purchase')}
|
||||
</button>
|
||||
) : (
|
||||
userPlan !== 'free' && (
|
||||
<button
|
||||
onClick={onManageSubscription}
|
||||
className='w-full rounded-lg bg-blue-100 px-6 py-3 font-medium text-blue-600 transition-colors hover:bg-blue-200 md:w-auto'
|
||||
>
|
||||
{_('Manage Subscription')}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button
|
||||
onClick={onLogout}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { IoCheckmark } from 'react-icons/io5';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { getLocale } from '@/utils/misc';
|
||||
import { PlanDetails } from '../utils/plan';
|
||||
import PlanActionButton from './PlanActionButton';
|
||||
|
||||
@@ -25,6 +26,10 @@ const PlanCard: React.FC<PlanCardProps> = ({
|
||||
onSelectPlan,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { price, currency } = plan;
|
||||
const formattedPrice = new Intl.NumberFormat(getLocale(), { style: 'currency', currency }).format(
|
||||
price / 100,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -38,7 +43,7 @@ const PlanCard: React.FC<PlanCardProps> = ({
|
||||
<div className='mb-6 text-center'>
|
||||
<h4 className='mb-2 text-2xl font-bold'>{_(plan.name)}</h4>
|
||||
<div className='text-3xl font-bold'>
|
||||
{`$${(plan.price / 100).toFixed(2)}`}
|
||||
{formattedPrice}
|
||||
<span className='text-lg font-normal'>/{_(plan.interval)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -107,11 +107,9 @@ const PlansComparison: React.FC<PlansComparisonProps> = ({
|
||||
}, [currentPlanIndex, handleScroll]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Current plan index:', currentPlanIndex);
|
||||
if (plansScrollRef.current) {
|
||||
const planWidth = plansScrollRef.current.scrollWidth / allPlans.length;
|
||||
const scrollPosition = currentPlanIndex * planWidth;
|
||||
console.log('Scroll position:', scrollPosition);
|
||||
plansScrollRef.current.scrollTo({
|
||||
left: scrollPosition,
|
||||
behavior: 'smooth',
|
||||
@@ -146,7 +144,7 @@ const PlansComparison: React.FC<PlansComparisonProps> = ({
|
||||
<PlanCard
|
||||
key={plan.plan}
|
||||
plan={plan}
|
||||
comingSoon={['appstore', 'playstore'].includes(appService?.distChannel || '')}
|
||||
comingSoon={['playstore'].includes(appService?.distChannel || '')}
|
||||
isUserPlan={plan.plan === userPlan}
|
||||
upgradable={index > userPlanIndex}
|
||||
index={index}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { getStripe } from '@/libs/stripe/client';
|
||||
import { getAPIBaseUrl, isTauriAppPlatform, isWebAppPlatform } from '@/services/environment';
|
||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
import { getAccessToken } from '@/utils/access';
|
||||
import { IAPService, IAPProduct } from '@/utils/iap';
|
||||
import { getPlanDetails } from './utils/plan';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import Spinner from '@/components/Spinner';
|
||||
@@ -32,14 +33,16 @@ import Checkout from './components/Checkout';
|
||||
const WEB_STRIPE_PLANS_URL = `${getAPIBaseUrl()}/stripe/plans`;
|
||||
const WEB_STRIPE_CHECKOUT_URL = `${getAPIBaseUrl()}/stripe/checkout`;
|
||||
const WEB_STRIPE_PORTAL_URL = `${getAPIBaseUrl()}/stripe/portal`;
|
||||
const SUBSCRIPTION_SUCCESS_PATH = '/user/subscription/success';
|
||||
|
||||
export type AvailablePlan = {
|
||||
plan: UserPlan;
|
||||
price_id: string;
|
||||
price: number;
|
||||
price: number; // in cents
|
||||
currency: string;
|
||||
interval: string;
|
||||
product: Stripe.Product;
|
||||
productName: string;
|
||||
product?: Stripe.Product;
|
||||
};
|
||||
|
||||
type CheckoutState = {
|
||||
@@ -100,7 +103,7 @@ const ProfilePage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubscribe = async (priceId?: string) => {
|
||||
const handleStripeSubscribe = async (priceId?: string) => {
|
||||
const token = await getAccessToken();
|
||||
const stripe = await getStripe();
|
||||
if (!stripe) {
|
||||
@@ -120,7 +123,7 @@ const ProfilePage = () => {
|
||||
setLoading(false);
|
||||
|
||||
const selectedPlan = availablePlans.find((plan) => plan.price_id === priceId)!;
|
||||
const planName = selectedPlan.product.name;
|
||||
const planName = selectedPlan.product?.name || selectedPlan.productName;
|
||||
if (isEmbeddedCheckout && sessionId && clientSecret) {
|
||||
setShowEmbeddedCheckout(true);
|
||||
setCheckoutState({
|
||||
@@ -153,11 +156,67 @@ const ProfilePage = () => {
|
||||
const handleCheckoutSuccess = useCallback(
|
||||
(sessionId: string) => {
|
||||
setShowEmbeddedCheckout(false);
|
||||
router.push(`/user/subscription/success?session_id=${sessionId}`);
|
||||
const params = new URLSearchParams({
|
||||
payment: 'stripe',
|
||||
session_id: sessionId,
|
||||
});
|
||||
router.push(`${SUBSCRIPTION_SUCCESS_PATH}?${params.toString()}`);
|
||||
},
|
||||
[router],
|
||||
);
|
||||
|
||||
const handleIAPSubscribe = async (productId?: string) => {
|
||||
if (!productId) return;
|
||||
|
||||
setLoading(true);
|
||||
const iapService = new IAPService();
|
||||
try {
|
||||
const purchase = await iapService.purchaseProduct(productId);
|
||||
if (purchase) {
|
||||
const params = new URLSearchParams({
|
||||
payment: 'iap',
|
||||
platform: purchase.platform,
|
||||
transaction_id: purchase.transactionId,
|
||||
original_transaction_id: purchase.originalTransactionId,
|
||||
});
|
||||
router.push(`${SUBSCRIPTION_SUCCESS_PATH}?${params.toString()}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('IAP purchase error:', error);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleIAPRestorePurchase = async () => {
|
||||
setLoading(true);
|
||||
const iapService = new IAPService();
|
||||
try {
|
||||
const purchases = await iapService.restorePurchases();
|
||||
if (purchases.length > 0) {
|
||||
const purchase = purchases[0]!;
|
||||
const params = new URLSearchParams({
|
||||
payment: 'iap',
|
||||
platform: purchase.platform,
|
||||
transaction_id: purchase.transactionId,
|
||||
original_transaction_id: purchase.originalTransactionId,
|
||||
});
|
||||
router.push(`${SUBSCRIPTION_SUCCESS_PATH}?${params.toString()}`);
|
||||
} else {
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'info',
|
||||
message: _('No purchases found to restore.'),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to restore purchases:', error);
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'info',
|
||||
message: _('Failed to restore purchases.'),
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleManageSubscription = async () => {
|
||||
setLoading(true);
|
||||
const token = await getAccessToken();
|
||||
@@ -175,8 +234,8 @@ const ProfilePage = () => {
|
||||
if (error) {
|
||||
console.error('Error creating portal session:', error);
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'error',
|
||||
message: _('Failed to manage subscription. Please try again later.'),
|
||||
type: 'info',
|
||||
message: _('Failed to manage subscription.'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -189,16 +248,53 @@ const ProfilePage = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetch(WEB_STRIPE_PLANS_URL)
|
||||
.then((res) => res.json())
|
||||
.then((data) => setAvailablePlans(data || []));
|
||||
}, []);
|
||||
if (!appService) return;
|
||||
|
||||
if (appService?.isIOSApp) {
|
||||
const iapService = new IAPService();
|
||||
iapService
|
||||
.initialize()
|
||||
.then(() =>
|
||||
iapService.fetchProducts([
|
||||
'com.bilingify.readest.monthly.plus',
|
||||
'com.bilingify.readest.monthly.pro',
|
||||
]),
|
||||
)
|
||||
.then((products: IAPProduct[]) => {
|
||||
const availablePlans: AvailablePlan[] = products.map((product) => ({
|
||||
plan: product.id.includes('plus')
|
||||
? 'plus'
|
||||
: product.id.includes('pro')
|
||||
? 'pro'
|
||||
: 'free',
|
||||
price_id: product.id,
|
||||
price: product.priceAmountMicros / 10000,
|
||||
currency: product.priceCurrencyCode || 'USD',
|
||||
interval: 'month',
|
||||
productName: product.title,
|
||||
}));
|
||||
setAvailablePlans(availablePlans);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to fetch IAP products:', error);
|
||||
eventDispatcher.dispatch('toast', {
|
||||
type: 'info',
|
||||
message: _('Failed to load subscription plans.'),
|
||||
});
|
||||
});
|
||||
} else {
|
||||
fetch(WEB_STRIPE_PLANS_URL)
|
||||
.then((res) => res.json())
|
||||
.then((data) => setAvailablePlans(data || []));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [appService]);
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!user || !token) {
|
||||
if (!user || !token || !appService) {
|
||||
return (
|
||||
<div className='mx-auto max-w-4xl px-4 py-8'>
|
||||
<div className='overflow-hidden rounded-lg shadow-md'>
|
||||
@@ -265,7 +361,7 @@ const ProfilePage = () => {
|
||||
<PlansComparison
|
||||
availablePlans={availablePlans}
|
||||
userPlan={userPlan}
|
||||
onSubscribe={handleSubscribe}
|
||||
onSubscribe={appService.isIOSApp ? handleIAPSubscribe : handleStripeSubscribe}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -274,6 +370,7 @@ const ProfilePage = () => {
|
||||
userPlan={userPlan}
|
||||
onLogout={handleLogout}
|
||||
onConfirmDelete={handleConfirmDelete}
|
||||
onRestorePurchase={handleIAPRestorePurchase}
|
||||
onManageSubscription={handleManageSubscription}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { supabase } from '@/utils/supabase';
|
||||
import Spinner from '@/components/Spinner';
|
||||
|
||||
const WEB_STRIPE_CHECK_URL = `${getAPIBaseUrl()}/stripe/check`;
|
||||
const WEB_APPLE_IAP_VERIFY_URL = `${getAPIBaseUrl()}/apple/iap-verify`;
|
||||
|
||||
interface SessionStatus {
|
||||
status: 'loading' | 'complete' | 'failed' | 'processing';
|
||||
@@ -27,12 +28,16 @@ const SuccessPageWithSearchParams = () => {
|
||||
const [retryCount, setRetryCount] = useState(0);
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const payment = searchParams?.get('payment');
|
||||
const platform = searchParams?.get('platform');
|
||||
const sessionId = searchParams?.get('session_id');
|
||||
const transactionId = searchParams?.get('transaction_id');
|
||||
const originalTransactionId = searchParams?.get('original_transaction_id');
|
||||
|
||||
const [mounted, setMounted] = useState(false);
|
||||
useEffect(() => setMounted(true), []);
|
||||
|
||||
async function fetchSessionStatus() {
|
||||
const updateStripeSessionStatus = async () => {
|
||||
try {
|
||||
const token = await getAccessToken();
|
||||
const response = await fetch(WEB_STRIPE_CHECK_URL, {
|
||||
@@ -74,12 +79,75 @@ const SuccessPageWithSearchParams = () => {
|
||||
console.error('Failed to fetch session status:', error);
|
||||
setSessionStatus((prev) => ({ ...prev, status: 'failed' }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updateIAPSessionStatus = async (
|
||||
platform: string,
|
||||
transactionId: string,
|
||||
originalTransactionId: string,
|
||||
) => {
|
||||
if (!transactionId || !originalTransactionId) {
|
||||
setSessionStatus((prev) => ({ ...prev, status: 'failed' }));
|
||||
return;
|
||||
}
|
||||
if (platform === 'ios') {
|
||||
try {
|
||||
const token = await getAccessToken();
|
||||
const response = await fetch(WEB_APPLE_IAP_VERIFY_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
transactionId,
|
||||
originalTransactionId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const { purchase, error } = await response.json();
|
||||
|
||||
if (error) {
|
||||
setSessionStatus((prev) => ({ ...prev, status: 'failed' }));
|
||||
console.error('IAP verification error:', error);
|
||||
return;
|
||||
}
|
||||
|
||||
setSessionStatus({
|
||||
status: purchase.status === 'active' ? 'complete' : 'processing',
|
||||
customerEmail: purchase.customerEmail || '',
|
||||
subscriptionId: purchase.subscriptionId,
|
||||
planName: purchase.planName,
|
||||
});
|
||||
|
||||
try {
|
||||
await supabase.auth.refreshSession();
|
||||
} catch {}
|
||||
} catch (error) {
|
||||
console.error('Failed to verify IAP transaction:', error);
|
||||
setSessionStatus((prev) => ({ ...prev, status: 'failed' }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updateSessionStatus = async () => {
|
||||
if (payment === 'stripe' && sessionId) {
|
||||
await updateStripeSessionStatus();
|
||||
} else if (payment === 'iap' && platform && transactionId && originalTransactionId) {
|
||||
await updateIAPSessionStatus(platform, transactionId, originalTransactionId);
|
||||
} else {
|
||||
setSessionStatus((prev) => ({ ...prev, status: 'failed' }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleRetry = () => {
|
||||
setRetryCount(0);
|
||||
setSessionStatus((prev) => ({ ...prev, status: 'loading' }));
|
||||
fetchSessionStatus();
|
||||
updateSessionStatus();
|
||||
};
|
||||
|
||||
const handleGoToLibrary = () => {
|
||||
@@ -91,20 +159,15 @@ const SuccessPageWithSearchParams = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!sessionId) {
|
||||
handleGoToProfile();
|
||||
return;
|
||||
}
|
||||
|
||||
fetchSessionStatus();
|
||||
updateSessionStatus();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sessionId, router]);
|
||||
}, [sessionId, originalTransactionId, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionStatus.status === 'processing' && retryCount < 3) {
|
||||
const timer = setTimeout(() => {
|
||||
setRetryCount((prev) => prev + 1);
|
||||
fetchSessionStatus();
|
||||
updateSessionStatus();
|
||||
}, 2000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
@@ -166,7 +229,7 @@ const SuccessPageWithSearchParams = () => {
|
||||
if (sessionStatus.status === 'failed') {
|
||||
return (
|
||||
<div className='flex min-h-screen items-center justify-center bg-gray-50'>
|
||||
<div className='max-w-md text-center'>
|
||||
<div className='mx-auto max-w-2xl px-4 text-center'>
|
||||
<div className='mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-red-100'>
|
||||
<svg
|
||||
className='h-6 w-6 text-red-600'
|
||||
@@ -244,7 +307,7 @@ const SuccessPageWithSearchParams = () => {
|
||||
{sessionStatus.planName && (
|
||||
<div className='flex justify-between'>
|
||||
<span className='font-medium'>{_('Plan:')}</span>
|
||||
<span>{sessionStatus.planName}</span>
|
||||
<span>{_(sessionStatus.planName)}</span>
|
||||
</div>
|
||||
)}
|
||||
{sessionStatus.amount && sessionStatus.currency && (
|
||||
|
||||
@@ -13,6 +13,7 @@ export type PlanDetails = {
|
||||
color: string;
|
||||
hintColor: string;
|
||||
price: number;
|
||||
currency: string;
|
||||
price_id?: string;
|
||||
interval: string;
|
||||
features: FeatureType[];
|
||||
@@ -22,8 +23,12 @@ export type PlanDetails = {
|
||||
export const getPlanDetails = (
|
||||
userPlan: UserPlan,
|
||||
availablePlans: AvailablePlan[],
|
||||
interval: 'month' | 'year' = 'month',
|
||||
): PlanDetails => {
|
||||
const availablePlan = availablePlans.find((plan) => plan.plan === userPlan);
|
||||
const availablePlan = availablePlans.find(
|
||||
(plan) => plan.plan === userPlan && (!plan.interval || plan.interval === interval),
|
||||
);
|
||||
const currency = availablePlans.length > 0 ? availablePlans[0]!.currency : 'USD';
|
||||
switch (userPlan) {
|
||||
case 'free':
|
||||
return {
|
||||
@@ -32,8 +37,9 @@ export const getPlanDetails = (
|
||||
color: 'bg-gray-200 text-gray-800',
|
||||
hintColor: 'text-gray-800/75',
|
||||
price: 0,
|
||||
currency,
|
||||
price_id: availablePlan?.price_id,
|
||||
interval: _('month'),
|
||||
interval: interval === 'month' ? _('month') : _('year'),
|
||||
features: [
|
||||
{
|
||||
label: _('Cross-Platform Sync'),
|
||||
@@ -78,8 +84,9 @@ export const getPlanDetails = (
|
||||
color: 'bg-blue-200 text-blue-800',
|
||||
hintColor: 'text-blue-800/75',
|
||||
price: availablePlan?.price || 499,
|
||||
currency,
|
||||
price_id: availablePlan?.price_id,
|
||||
interval: _('month'),
|
||||
interval: interval === 'month' ? _('month') : _('year'),
|
||||
features: [
|
||||
{
|
||||
label: _('Includes All Free Plan Benefits'),
|
||||
@@ -127,8 +134,9 @@ export const getPlanDetails = (
|
||||
color: 'bg-purple-200 text-purple-800',
|
||||
hintColor: 'text-purple-800/75',
|
||||
price: availablePlan?.price || 999,
|
||||
currency,
|
||||
price_id: availablePlan?.price_id,
|
||||
interval: _('month'),
|
||||
interval: interval === 'month' ? _('month') : _('year'),
|
||||
features: [
|
||||
{
|
||||
label: _('Includes All Plus Plan Benefits'),
|
||||
|
||||
@@ -12,9 +12,9 @@ export const Toast = () => {
|
||||
const toastDismissTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const toastClassMap = {
|
||||
info: 'toast-info toast-center toast-middle',
|
||||
success: 'toast-success toast-top toast-end',
|
||||
warning: 'toast-warning toast-top toast-end',
|
||||
error: 'toast-error toast-top toast-end',
|
||||
success: 'toast-success toast-top sm:toast-end toast-center',
|
||||
warning: 'toast-warning toast-top sm:toast-end toast-center',
|
||||
error: 'toast-error toast-top sm:toast-end toast-center',
|
||||
};
|
||||
const alertClassMap = {
|
||||
info: 'alert-primary border-base-300',
|
||||
@@ -53,7 +53,7 @@ export const Toast = () => {
|
||||
'toast toast-center toast-middle z-50 w-auto max-w-screen-sm',
|
||||
toastClassMap[toastType.current],
|
||||
toastClassMap[toastType.current].includes('toast-top') &&
|
||||
'pt-[calc(44px+env(safe-area-inset-top))]',
|
||||
'top-[calc(44px+env(safe-area-inset-top))]',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
@@ -64,11 +64,11 @@ export const Toast = () => {
|
||||
>
|
||||
<span
|
||||
className={clsx(
|
||||
'max-h-[50vh] min-w-32',
|
||||
'overflow-y-auto text-center',
|
||||
'max-h-[50vh] min-w-32 overflow-y-auto',
|
||||
'text-center font-sans text-base font-normal sm:text-sm',
|
||||
toastType.current === 'info'
|
||||
? 'max-w-[80vw]'
|
||||
: 'max-w-80 whitespace-normal break-words',
|
||||
: 'min-w-[60vw] max-w-[80vw] whitespace-normal break-words sm:min-w-40 sm:max-w-80',
|
||||
messageClass.current,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { IAPError } from '@/types/error';
|
||||
import {
|
||||
AppStoreServerAPI,
|
||||
Environment,
|
||||
@@ -58,7 +59,7 @@ export class AppleIAPVerifier {
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
error: error instanceof Error ? error.message : IAPError.UNKNOWN_ERROR,
|
||||
};
|
||||
}
|
||||
if (response.data && response.data.length > 0) {
|
||||
@@ -69,7 +70,7 @@ export class AppleIAPVerifier {
|
||||
if (!lastTransaction) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Transaction not found',
|
||||
error: IAPError.TRANSACTION_NOT_FOUND,
|
||||
};
|
||||
}
|
||||
const decodedTransaction = await decodeTransaction(lastTransaction.signedTransactionInfo);
|
||||
@@ -109,7 +110,7 @@ export class AppleIAPVerifier {
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: 'No transactions found for this original transaction ID',
|
||||
error: IAPError.TRANSACTION_NOT_FOUND,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export enum IAPError {
|
||||
INVALID_INPUT = 'Invalid input data',
|
||||
NOT_AUTHENTICATED = 'Not authenticated',
|
||||
TRANSACTION_NOT_FOUND = 'Transaction not found',
|
||||
TRANSACTION_BELONGS_TO_ANOTHER_USER = 'This transaction does not belong to the authenticated user',
|
||||
TRANSACTION_SERVICE_UNAVAILABLE = 'Transaction service is currently unavailable. Please contact support.',
|
||||
RESTORE_FAILED = 'Failed to restore purchases. Please try again later.',
|
||||
UNKNOWN_ERROR = 'Unknown error',
|
||||
}
|
||||
+1
-1
Submodule packages/tauri updated: b45b75a13f...3539f64bfb
+1
-1
Submodule packages/tauri-plugins updated: f1202ecfff...42f62f9d78
Reference in New Issue
Block a user