feat: enable IAP for upgrading to Readest Premium on iOS (#1678)

This commit is contained in:
Huang Xin
2025-07-26 01:08:53 +08:00
committed by GitHub
parent 8974a87168
commit 42c2825e90
35 changed files with 519 additions and 111 deletions
Generated
+102 -5
View File
@@ -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
View File
@@ -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 labonnement",
"Failed to manage subscription. Please try again later.": "Échec de la gestion de labonnement. 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 },
);
}
+1 -2
View File
@@ -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}
+110 -13
View File
@@ -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 && (
+12 -4
View File
@@ -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'),
+7 -7
View File
@@ -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,
};
}
}
+9
View File
@@ -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',
}