feat(applock): biometric unlock (fingerprint / Face ID) at startup on mobile (#4650)
* feat(applock): wire up biometric plugin + biometricUnlockEnabled setting Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(applock): add guarded biometric service wrapper * feat(applock): auto-prompt biometrics on the lock screen with PIN fallback Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(applock): guard concurrent biometric prompts + tighten lock-screen tests - Add biometricInFlightRef to prevent concurrent authenticateWithBiometrics calls - Assert PIN input still rendered after biometric failure (test 2) - Replace flaky waitFor negative assertion with a 50ms flush in test 3 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(applock): mobile biometric toggle + default-on at PIN setup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(i18n): add biometric app-lock strings * fix(applock): seed biometricUnlockEnabled via app-lock store init; close test gaps Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * build(applock): pin tauri-plugin-biometric in Cargo.lock Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Generated
+16
@@ -40,6 +40,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build 2.6.2",
|
||||
"tauri-plugin-biometric",
|
||||
"tauri-plugin-cli",
|
||||
"tauri-plugin-clipboard-manager",
|
||||
"tauri-plugin-deep-link",
|
||||
@@ -7694,6 +7695,21 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-biometric"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "306175b744890b5e4aeb8add6aae7debec1b1504c5087bd0310ed7c9c6feae38"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-cli"
|
||||
version = "2.4.1"
|
||||
|
||||
@@ -129,6 +129,7 @@
|
||||
"@supabase/auth-ui-shared": "^0.1.8",
|
||||
"@supabase/supabase-js": "^2.76.1",
|
||||
"@tauri-apps/api": "2.10.1",
|
||||
"@tauri-apps/plugin-biometric": "^2.3.2",
|
||||
"@tauri-apps/plugin-cli": "^2.4.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
|
||||
"@tauri-apps/plugin-deep-link": "^2.4.7",
|
||||
|
||||
@@ -1898,5 +1898,12 @@
|
||||
"Erase the book and all its data, including reading progress": "محو الكتاب وجميع بياناته، بما في ذلك تقدم القراءة",
|
||||
"More Actions": "إجراءات أخرى",
|
||||
"Sign in and make the book available to share it": "سجّل الدخول واجعل الكتاب متاحًا لمشاركته",
|
||||
"Download the book to export it": "نزّل الكتاب لتصديره"
|
||||
"Download the book to export it": "نزّل الكتاب لتصديره",
|
||||
"Unlock Readest": "فتح قفل Readest",
|
||||
"Use {{biometry}}": "استخدام {{biometry}}",
|
||||
"Unlock with {{biometry}}": "فتح القفل بـ {{biometry}}",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "يتطلب رمز PIN (والبيومترية، إن توفّرت) لفتح Readest",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "البيومترية"
|
||||
}
|
||||
|
||||
@@ -1766,5 +1766,12 @@
|
||||
"Erase the book and all its data, including reading progress": "পড়ার অগ্রগতিসহ বই ও এর সমস্ত ডেটা মুছে ফেলুন",
|
||||
"More Actions": "আরও ক্রিয়া",
|
||||
"Sign in and make the book available to share it": "শেয়ার করতে সাইন ইন করুন এবং বইটি উপলব্ধ করুন",
|
||||
"Download the book to export it": "এক্সপোর্ট করতে বইটি ডাউনলোড করুন"
|
||||
"Download the book to export it": "এক্সপোর্ট করতে বইটি ডাউনলোড করুন",
|
||||
"Unlock Readest": "Readest আনলক করুন",
|
||||
"Use {{biometry}}": "{{biometry}} ব্যবহার করুন",
|
||||
"Unlock with {{biometry}}": "{{biometry}} দিয়ে আনলক করুন",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "Readest খুলতে PIN (এবং উপলব্ধ হলে বায়োমেট্রিক্স) প্রয়োজন",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "বায়োমেট্রিক্স"
|
||||
}
|
||||
|
||||
@@ -1733,5 +1733,12 @@
|
||||
"Erase the book and all its data, including reading progress": "དཔེ་དེབ་དང་ཀློག་འདོན་གྱི་འཕེལ་རིམ་བཅས་ཀྱི་གཞི་གྲངས་ཡོངས་སུ་བསུབ།",
|
||||
"More Actions": "བྱ་སྤྱོད་གཞན་དག",
|
||||
"Sign in and make the book available to share it": "ནང་འཛུལ་བྱས་ནས་དཔེ་དེབ་མཁོ་སྤྲོད་རུང་བར་བཟོས་ནས་མཉམ་སྤྱོད་བྱོས།",
|
||||
"Download the book to export it": "ཕྱིར་འདྲེན་བྱེད་པར་དཔེ་དེབ་ཕབ་ལེན་བྱོས།"
|
||||
"Download the book to export it": "ཕྱིར་འདྲེན་བྱེད་པར་དཔེ་དེབ་ཕབ་ལེན་བྱོས།",
|
||||
"Unlock Readest": "Readest སྒོ་ལྕགས་ཕྱེ།",
|
||||
"Use {{biometry}}": "{{biometry}} བེད་སྤྱོད།",
|
||||
"Unlock with {{biometry}}": "{{biometry}} ཐོག་ནས་སྒོ་ལྕགས་ཕྱེ།",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "Readest ཕྱེ་བར་ PIN (དང་གལ་ཏེ་ཡོད་ན་ལུས་རྟགས་ངོ་སྤྲོད།) དགོས།",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "ལུས་རྟགས་ངོ་སྤྲོད།"
|
||||
}
|
||||
|
||||
@@ -1766,5 +1766,12 @@
|
||||
"Erase the book and all its data, including reading progress": "Buch und alle Daten löschen, einschließlich Lesefortschritt",
|
||||
"More Actions": "Weitere Aktionen",
|
||||
"Sign in and make the book available to share it": "Melde dich an und mache das Buch verfügbar, um es zu teilen",
|
||||
"Download the book to export it": "Lade das Buch herunter, um es zu exportieren"
|
||||
"Download the book to export it": "Lade das Buch herunter, um es zu exportieren",
|
||||
"Unlock Readest": "Readest entsperren",
|
||||
"Use {{biometry}}": "{{biometry}} verwenden",
|
||||
"Unlock with {{biometry}}": "Mit {{biometry}} entsperren",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "PIN (und Biometrie, falls verfügbar) zum Öffnen von Readest erforderlich",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "Biometrie"
|
||||
}
|
||||
|
||||
@@ -1766,5 +1766,12 @@
|
||||
"Erase the book and all its data, including reading progress": "Διαγράφει το βιβλίο και όλα τα δεδομένα του, συμπεριλαμβανομένης της προόδου ανάγνωσης",
|
||||
"More Actions": "Περισσότερες ενέργειες",
|
||||
"Sign in and make the book available to share it": "Συνδεθείτε και κάντε το βιβλίο διαθέσιμο για να το μοιραστείτε",
|
||||
"Download the book to export it": "Κατεβάστε το βιβλίο για να το εξαγάγετε"
|
||||
"Download the book to export it": "Κατεβάστε το βιβλίο για να το εξαγάγετε",
|
||||
"Unlock Readest": "Ξεκλείδωμα του Readest",
|
||||
"Use {{biometry}}": "Χρήση {{biometry}}",
|
||||
"Unlock with {{biometry}}": "Ξεκλείδωμα με {{biometry}}",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "Απαιτείται PIN (και βιομετρικά, εάν διατίθενται) για το άνοιγμα του Readest",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "βιομετρικά"
|
||||
}
|
||||
|
||||
@@ -1799,5 +1799,12 @@
|
||||
"Erase the book and all its data, including reading progress": "Borra el libro y todos sus datos, incluido el progreso de lectura",
|
||||
"More Actions": "Más acciones",
|
||||
"Sign in and make the book available to share it": "Inicia sesión y ten el libro disponible para compartirlo",
|
||||
"Download the book to export it": "Descarga el libro para exportarlo"
|
||||
"Download the book to export it": "Descarga el libro para exportarlo",
|
||||
"Unlock Readest": "Desbloquear Readest",
|
||||
"Use {{biometry}}": "Usar {{biometry}}",
|
||||
"Unlock with {{biometry}}": "Desbloquear con {{biometry}}",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "Requiere un PIN (y biometría, si está disponible) para abrir Readest",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "biometría"
|
||||
}
|
||||
|
||||
@@ -1766,5 +1766,12 @@
|
||||
"Erase the book and all its data, including reading progress": "کتاب و تمام دادههای آن، شامل پیشرفت مطالعه را پاک میکند",
|
||||
"More Actions": "اقدامات بیشتر",
|
||||
"Sign in and make the book available to share it": "برای اشتراکگذاری، وارد شوید و کتاب را در دسترس قرار دهید",
|
||||
"Download the book to export it": "برای خروجی گرفتن، کتاب را دانلود کنید"
|
||||
"Download the book to export it": "برای خروجی گرفتن، کتاب را دانلود کنید",
|
||||
"Unlock Readest": "باز کردن قفل Readest",
|
||||
"Use {{biometry}}": "استفاده از {{biometry}}",
|
||||
"Unlock with {{biometry}}": "باز کردن قفل با {{biometry}}",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "برای باز کردن Readest به PIN (و در صورت موجود بودن، بیومتریک) نیاز است",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "بیومتریک"
|
||||
}
|
||||
|
||||
@@ -1799,5 +1799,12 @@
|
||||
"Erase the book and all its data, including reading progress": "Supprime le livre et toutes ses données, y compris la progression de lecture",
|
||||
"More Actions": "Plus d'actions",
|
||||
"Sign in and make the book available to share it": "Connectez-vous et rendez le livre disponible pour le partager",
|
||||
"Download the book to export it": "Téléchargez le livre pour l'exporter"
|
||||
"Download the book to export it": "Téléchargez le livre pour l'exporter",
|
||||
"Unlock Readest": "Déverrouiller Readest",
|
||||
"Use {{biometry}}": "Utiliser {{biometry}}",
|
||||
"Unlock with {{biometry}}": "Déverrouiller avec {{biometry}}",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "Exiger un code PIN (et la biométrie, si disponible) pour ouvrir Readest",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "biométrie"
|
||||
}
|
||||
|
||||
@@ -1799,5 +1799,12 @@
|
||||
"Erase the book and all its data, including reading progress": "מחיקת הספר וכל הנתונים שלו, כולל התקדמות הקריאה",
|
||||
"More Actions": "פעולות נוספות",
|
||||
"Sign in and make the book available to share it": "היכנס והפוך את הספר לזמין כדי לשתף אותו",
|
||||
"Download the book to export it": "הורד את הספר כדי לייצא אותו"
|
||||
"Download the book to export it": "הורד את הספר כדי לייצא אותו",
|
||||
"Unlock Readest": "פתח את Readest",
|
||||
"Use {{biometry}}": "השתמש ב-{{biometry}}",
|
||||
"Unlock with {{biometry}}": "פתח עם {{biometry}}",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "דרוש קוד PIN (וביומטריה, אם זמינה) כדי לפתוח את Readest",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "ביומטריה"
|
||||
}
|
||||
|
||||
@@ -1766,5 +1766,12 @@
|
||||
"Erase the book and all its data, including reading progress": "पढ़ने की प्रगति सहित पुस्तक और उसका सारा डेटा मिटाएँ",
|
||||
"More Actions": "अधिक क्रियाएँ",
|
||||
"Sign in and make the book available to share it": "साझा करने के लिए साइन इन करें और पुस्तक उपलब्ध कराएँ",
|
||||
"Download the book to export it": "निर्यात करने के लिए पुस्तक डाउनलोड करें"
|
||||
"Download the book to export it": "निर्यात करने के लिए पुस्तक डाउनलोड करें",
|
||||
"Unlock Readest": "Readest अनलॉक करें",
|
||||
"Use {{biometry}}": "{{biometry}} उपयोग करें",
|
||||
"Unlock with {{biometry}}": "{{biometry}} से अनलॉक करें",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "Readest खोलने के लिए PIN (और यदि उपलब्ध हो तो बायोमेट्रिक्स) आवश्यक है",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "बायोमेट्रिक्स"
|
||||
}
|
||||
|
||||
@@ -1766,5 +1766,12 @@
|
||||
"Erase the book and all its data, including reading progress": "Törli a könyvet és minden adatát, beleértve az olvasási előrehaladást",
|
||||
"More Actions": "További műveletek",
|
||||
"Sign in and make the book available to share it": "Jelentkezz be, és tedd elérhetővé a könyvet a megosztáshoz",
|
||||
"Download the book to export it": "Töltsd le a könyvet az exportáláshoz"
|
||||
"Download the book to export it": "Töltsd le a könyvet az exportáláshoz",
|
||||
"Unlock Readest": "Readest feloldása",
|
||||
"Use {{biometry}}": "{{biometry}} használata",
|
||||
"Unlock with {{biometry}}": "Feloldás {{biometry}} segítségével",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "PIN-kód (és biometria, ha elérhető) szükséges a Readest megnyitásához",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "biometria"
|
||||
}
|
||||
|
||||
@@ -1733,5 +1733,12 @@
|
||||
"Erase the book and all its data, including reading progress": "Menghapus buku dan semua datanya, termasuk progres baca",
|
||||
"More Actions": "Tindakan lainnya",
|
||||
"Sign in and make the book available to share it": "Masuk dan sediakan buku untuk membagikannya",
|
||||
"Download the book to export it": "Unduh buku untuk mengekspornya"
|
||||
"Download the book to export it": "Unduh buku untuk mengekspornya",
|
||||
"Unlock Readest": "Buka kunci Readest",
|
||||
"Use {{biometry}}": "Gunakan {{biometry}}",
|
||||
"Unlock with {{biometry}}": "Buka kunci dengan {{biometry}}",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "Memerlukan PIN (dan biometrik, jika tersedia) untuk membuka Readest",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "biometrik"
|
||||
}
|
||||
|
||||
@@ -1799,5 +1799,12 @@
|
||||
"Erase the book and all its data, including reading progress": "Elimina il libro e tutti i suoi dati, inclusi i progressi di lettura",
|
||||
"More Actions": "Altre azioni",
|
||||
"Sign in and make the book available to share it": "Accedi e rendi disponibile il libro per condividerlo",
|
||||
"Download the book to export it": "Scarica il libro per esportarlo"
|
||||
"Download the book to export it": "Scarica il libro per esportarlo",
|
||||
"Unlock Readest": "Sblocca Readest",
|
||||
"Use {{biometry}}": "Usa {{biometry}}",
|
||||
"Unlock with {{biometry}}": "Sblocca con {{biometry}}",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "Richiede un PIN (e la biometria, se disponibile) per aprire Readest",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "biometria"
|
||||
}
|
||||
|
||||
@@ -1733,5 +1733,12 @@
|
||||
"Erase the book and all its data, including reading progress": "本と読書の進行状況を含むすべてのデータを消去します",
|
||||
"More Actions": "その他の操作",
|
||||
"Sign in and make the book available to share it": "共有するにはサインインして書籍を利用可能にしてください",
|
||||
"Download the book to export it": "エクスポートするには書籍をダウンロードしてください"
|
||||
"Download the book to export it": "エクスポートするには書籍をダウンロードしてください",
|
||||
"Unlock Readest": "Readest のロックを解除",
|
||||
"Use {{biometry}}": "{{biometry}} を使用",
|
||||
"Unlock with {{biometry}}": "{{biometry}} でロック解除",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "Readest を開くには PIN(および利用可能な場合は生体認証)が必要です",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "生体認証"
|
||||
}
|
||||
|
||||
@@ -1733,5 +1733,12 @@
|
||||
"Erase the book and all its data, including reading progress": "읽기 진행률을 포함한 책과 모든 데이터를 삭제합니다",
|
||||
"More Actions": "추가 작업",
|
||||
"Sign in and make the book available to share it": "공유하려면 로그인하고 책을 사용할 수 있도록 하세요",
|
||||
"Download the book to export it": "내보내려면 책을 다운로드하세요"
|
||||
"Download the book to export it": "내보내려면 책을 다운로드하세요",
|
||||
"Unlock Readest": "Readest 잠금 해제",
|
||||
"Use {{biometry}}": "{{biometry}} 사용",
|
||||
"Unlock with {{biometry}}": "{{biometry}}로 잠금 해제",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "Readest를 열려면 PIN(및 사용 가능한 경우 생체 인증)이 필요합니다",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "생체 인증"
|
||||
}
|
||||
|
||||
@@ -1733,5 +1733,12 @@
|
||||
"Erase the book and all its data, including reading progress": "Memadam buku dan semua datanya, termasuk kemajuan bacaan",
|
||||
"More Actions": "Tindakan lain",
|
||||
"Sign in and make the book available to share it": "Log masuk dan sediakan buku untuk berkongsinya",
|
||||
"Download the book to export it": "Muat turun buku untuk mengeksportnya"
|
||||
"Download the book to export it": "Muat turun buku untuk mengeksportnya",
|
||||
"Unlock Readest": "Buka kunci Readest",
|
||||
"Use {{biometry}}": "Guna {{biometry}}",
|
||||
"Unlock with {{biometry}}": "Buka kunci dengan {{biometry}}",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "Memerlukan PIN (dan biometrik, jika tersedia) untuk membuka Readest",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "biometrik"
|
||||
}
|
||||
|
||||
@@ -1766,5 +1766,12 @@
|
||||
"Erase the book and all its data, including reading progress": "Wist het boek en alle gegevens, inclusief leesvoortgang",
|
||||
"More Actions": "Meer acties",
|
||||
"Sign in and make the book available to share it": "Meld je aan en maak het boek beschikbaar om het te delen",
|
||||
"Download the book to export it": "Download het boek om het te exporteren"
|
||||
"Download the book to export it": "Download het boek om het te exporteren",
|
||||
"Unlock Readest": "Readest ontgrendelen",
|
||||
"Use {{biometry}}": "{{biometry}} gebruiken",
|
||||
"Unlock with {{biometry}}": "Ontgrendelen met {{biometry}}",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "Vereist een pincode (en biometrie, indien beschikbaar) om Readest te openen",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "biometrie"
|
||||
}
|
||||
|
||||
@@ -1832,5 +1832,12 @@
|
||||
"Erase the book and all its data, including reading progress": "Usuwa książkę i wszystkie jej dane, w tym postęp czytania",
|
||||
"More Actions": "Więcej akcji",
|
||||
"Sign in and make the book available to share it": "Zaloguj się i pobierz książkę, aby ją udostępnić",
|
||||
"Download the book to export it": "Pobierz książkę, aby ją wyeksportować"
|
||||
"Download the book to export it": "Pobierz książkę, aby ją wyeksportować",
|
||||
"Unlock Readest": "Odblokuj Readest",
|
||||
"Use {{biometry}}": "Użyj {{biometry}}",
|
||||
"Unlock with {{biometry}}": "Odblokuj za pomocą {{biometry}}",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "Wymaga kodu PIN (i biometrii, jeśli dostępna) do otwarcia Readest",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "biometria"
|
||||
}
|
||||
|
||||
@@ -1799,5 +1799,12 @@
|
||||
"Erase the book and all its data, including reading progress": "Apaga o livro e todos os seus dados, incluindo o progresso de leitura",
|
||||
"More Actions": "Mais ações",
|
||||
"Sign in and make the book available to share it": "Faça login e tenha o livro disponível para compartilhá-lo",
|
||||
"Download the book to export it": "Baixe o livro para exportá-lo"
|
||||
"Download the book to export it": "Baixe o livro para exportá-lo",
|
||||
"Unlock Readest": "Desbloquear o Readest",
|
||||
"Use {{biometry}}": "Usar {{biometry}}",
|
||||
"Unlock with {{biometry}}": "Desbloquear com {{biometry}}",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "Requer um PIN (e biometria, se disponível) para abrir o Readest",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "biometria"
|
||||
}
|
||||
|
||||
@@ -1799,5 +1799,12 @@
|
||||
"Erase the book and all its data, including reading progress": "Apaga o livro e todos os seus dados, incluindo o progresso de leitura",
|
||||
"More Actions": "Mais ações",
|
||||
"Sign in and make the book available to share it": "Inicie sessão e tenha o livro disponível para o partilhar",
|
||||
"Download the book to export it": "Transfira o livro para o exportar"
|
||||
"Download the book to export it": "Transfira o livro para o exportar",
|
||||
"Unlock Readest": "Desbloquear o Readest",
|
||||
"Use {{biometry}}": "Usar {{biometry}}",
|
||||
"Unlock with {{biometry}}": "Desbloquear com {{biometry}}",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "Requer um PIN (e biometria, se disponível) para abrir o Readest",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "biometria"
|
||||
}
|
||||
|
||||
@@ -1799,5 +1799,12 @@
|
||||
"Erase the book and all its data, including reading progress": "Șterge cartea și toate datele sale, inclusiv progresul lecturii",
|
||||
"More Actions": "Mai multe acțiuni",
|
||||
"Sign in and make the book available to share it": "Conectează-te și fă cartea disponibilă pentru a o partaja",
|
||||
"Download the book to export it": "Descarcă cartea pentru a o exporta"
|
||||
"Download the book to export it": "Descarcă cartea pentru a o exporta",
|
||||
"Unlock Readest": "Deblochează Readest",
|
||||
"Use {{biometry}}": "Folosește {{biometry}}",
|
||||
"Unlock with {{biometry}}": "Deblochează cu {{biometry}}",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "Necesită un PIN (și biometrie, dacă este disponibilă) pentru a deschide Readest",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "biometrie"
|
||||
}
|
||||
|
||||
@@ -1832,5 +1832,12 @@
|
||||
"Erase the book and all its data, including reading progress": "Удаляет книгу и все её данные, включая прогресс чтения",
|
||||
"More Actions": "Ещё действия",
|
||||
"Sign in and make the book available to share it": "Войдите и сделайте книгу доступной, чтобы поделиться ею",
|
||||
"Download the book to export it": "Скачайте книгу, чтобы экспортировать её"
|
||||
"Download the book to export it": "Скачайте книгу, чтобы экспортировать её",
|
||||
"Unlock Readest": "Разблокировать Readest",
|
||||
"Use {{biometry}}": "Использовать {{biometry}}",
|
||||
"Unlock with {{biometry}}": "Разблокировать с помощью {{biometry}}",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "Требуется PIN-код (и биометрия, если доступна) для открытия Readest",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "биометрия"
|
||||
}
|
||||
|
||||
@@ -1766,5 +1766,12 @@
|
||||
"Erase the book and all its data, including reading progress": "කියවීමේ ප්රගතිය ඇතුළුව පොත සහ එහි සියලු දත්ත මකන්න",
|
||||
"More Actions": "තවත් ක්රියා",
|
||||
"Sign in and make the book available to share it": "බෙදා ගැනීමට පුරනය වී පොත ලබා ගත හැකි කරන්න",
|
||||
"Download the book to export it": "අපනයනය කිරීමට පොත බාගන්න"
|
||||
"Download the book to export it": "අපනයනය කිරීමට පොත බාගන්න",
|
||||
"Unlock Readest": "Readest අඟුල් හරිනු",
|
||||
"Use {{biometry}}": "{{biometry}} භාවිත කරන්න",
|
||||
"Unlock with {{biometry}}": "{{biometry}} සමඟ අඟුල් හරිනු",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "Readest විවෘත කිරීමට PIN (සහ ජෛව මිතික, ලබා ගත හැකි නම්) අවශ්ය වේ",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "ජෛව මිතික"
|
||||
}
|
||||
|
||||
@@ -1832,5 +1832,12 @@
|
||||
"Erase the book and all its data, including reading progress": "Izbriše knjigo in vse njene podatke, vključno z napredkom branja",
|
||||
"More Actions": "Več dejanj",
|
||||
"Sign in and make the book available to share it": "Prijavite se in omogočite knjigo za deljenje",
|
||||
"Download the book to export it": "Prenesite knjigo, da jo izvozite"
|
||||
"Download the book to export it": "Prenesite knjigo, da jo izvozite",
|
||||
"Unlock Readest": "Odkleni Readest",
|
||||
"Use {{biometry}}": "Uporabi {{biometry}}",
|
||||
"Unlock with {{biometry}}": "Odkleni z {{biometry}}",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "Za odpiranje Readest je potrebna PIN-koda (in biometrija, če je na voljo)",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "biometrija"
|
||||
}
|
||||
|
||||
@@ -1766,5 +1766,12 @@
|
||||
"Erase the book and all its data, including reading progress": "Raderar boken och all data, inklusive läsförlopp",
|
||||
"More Actions": "Fler åtgärder",
|
||||
"Sign in and make the book available to share it": "Logga in och gör boken tillgänglig för att dela den",
|
||||
"Download the book to export it": "Ladda ner boken för att exportera den"
|
||||
"Download the book to export it": "Ladda ner boken för att exportera den",
|
||||
"Unlock Readest": "Lås upp Readest",
|
||||
"Use {{biometry}}": "Använd {{biometry}}",
|
||||
"Unlock with {{biometry}}": "Lås upp med {{biometry}}",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "Kräver en PIN-kod (och biometri, om tillgängligt) för att öppna Readest",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "biometri"
|
||||
}
|
||||
|
||||
@@ -1766,5 +1766,12 @@
|
||||
"Erase the book and all its data, including reading progress": "வாசிப்பு முன்னேற்றம் உட்பட புத்தகத்தையும் அதன் எல்லா தரவையும் அழிக்கும்",
|
||||
"More Actions": "மேலும் செயல்கள்",
|
||||
"Sign in and make the book available to share it": "பகிர உள்நுழைந்து புத்தகத்தைக் கிடைக்கச் செய்யவும்",
|
||||
"Download the book to export it": "ஏற்றுமதி செய்ய புத்தகத்தைப் பதிவிறக்கவும்"
|
||||
"Download the book to export it": "ஏற்றுமதி செய்ய புத்தகத்தைப் பதிவிறக்கவும்",
|
||||
"Unlock Readest": "Readest-ஐ திறக்கவும்",
|
||||
"Use {{biometry}}": "{{biometry}} பயன்படுத்தவும்",
|
||||
"Unlock with {{biometry}}": "{{biometry}} மூலம் திறக்கவும்",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "Readest திறக்க PIN (மற்றும் கிடைத்தால் உயிரியல் அடையாளம்) தேவை",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "உயிரியல் அடையாளம்"
|
||||
}
|
||||
|
||||
@@ -1733,5 +1733,12 @@
|
||||
"Erase the book and all its data, including reading progress": "ลบหนังสือและข้อมูลทั้งหมด รวมถึงความคืบหน้าการอ่าน",
|
||||
"More Actions": "การทำงานเพิ่มเติม",
|
||||
"Sign in and make the book available to share it": "ลงชื่อเข้าใช้และทำให้หนังสือพร้อมใช้งานเพื่อแชร์",
|
||||
"Download the book to export it": "ดาวน์โหลดหนังสือเพื่อส่งออก"
|
||||
"Download the book to export it": "ดาวน์โหลดหนังสือเพื่อส่งออก",
|
||||
"Unlock Readest": "ปลดล็อก Readest",
|
||||
"Use {{biometry}}": "ใช้ {{biometry}}",
|
||||
"Unlock with {{biometry}}": "ปลดล็อกด้วย {{biometry}}",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "ต้องใช้ PIN (และข้อมูลไบโอเมตริก หากมี) เพื่อเปิด Readest",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "ข้อมูลไบโอเมตริก"
|
||||
}
|
||||
|
||||
@@ -1766,5 +1766,12 @@
|
||||
"Erase the book and all its data, including reading progress": "Kitabı ve okuma ilerlemesi dahil tüm verilerini siler",
|
||||
"More Actions": "Diğer eylemler",
|
||||
"Sign in and make the book available to share it": "Paylaşmak için oturum açın ve kitabı kullanılabilir hale getirin",
|
||||
"Download the book to export it": "Dışa aktarmak için kitabı indirin"
|
||||
"Download the book to export it": "Dışa aktarmak için kitabı indirin",
|
||||
"Unlock Readest": "Readest'i Kilidi Aç",
|
||||
"Use {{biometry}}": "{{biometry}} Kullan",
|
||||
"Unlock with {{biometry}}": "{{biometry}} ile Kilidi Aç",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "Readest'i açmak için PIN (ve varsa biyometri) gereklidir",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "biyometri"
|
||||
}
|
||||
|
||||
@@ -1832,5 +1832,12 @@
|
||||
"Erase the book and all its data, including reading progress": "Видаляє книгу та всі її дані, зокрема прогрес читання",
|
||||
"More Actions": "Більше дій",
|
||||
"Sign in and make the book available to share it": "Увійдіть і зробіть книгу доступною, щоб поділитися нею",
|
||||
"Download the book to export it": "Завантажте книгу, щоб експортувати її"
|
||||
"Download the book to export it": "Завантажте книгу, щоб експортувати її",
|
||||
"Unlock Readest": "Розблокувати Readest",
|
||||
"Use {{biometry}}": "Використати {{biometry}}",
|
||||
"Unlock with {{biometry}}": "Розблокувати за допомогою {{biometry}}",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "Потрібен PIN-код (і біометрія, якщо доступна) для відкриття Readest",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "біометрія"
|
||||
}
|
||||
|
||||
@@ -1766,5 +1766,12 @@
|
||||
"Erase the book and all its data, including reading progress": "Kitobni va uning barcha maʼlumotlarini, jumladan oʻqish jarayonini oʻchiradi",
|
||||
"More Actions": "Qoʻshimcha amallar",
|
||||
"Sign in and make the book available to share it": "Ulashish uchun tizimga kiring va kitobni mavjud qiling",
|
||||
"Download the book to export it": "Eksport qilish uchun kitobni yuklab oling"
|
||||
"Download the book to export it": "Eksport qilish uchun kitobni yuklab oling",
|
||||
"Unlock Readest": "Readest-ni ochish",
|
||||
"Use {{biometry}}": "{{biometry}} dan foydalanish",
|
||||
"Unlock with {{biometry}}": "{{biometry}} bilan ochish",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "Readest-ni ochish uchun PIN (va mavjud bo'lsa, biometrik ma'lumotlar) talab qilinadi",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "biometrik"
|
||||
}
|
||||
|
||||
@@ -1733,5 +1733,12 @@
|
||||
"Erase the book and all its data, including reading progress": "Xóa sách và toàn bộ dữ liệu, bao gồm tiến độ đọc",
|
||||
"More Actions": "Thao tác khác",
|
||||
"Sign in and make the book available to share it": "Đăng nhập và tải sách về để chia sẻ",
|
||||
"Download the book to export it": "Tải sách về để xuất ra"
|
||||
"Download the book to export it": "Tải sách về để xuất ra",
|
||||
"Unlock Readest": "Mở khóa Readest",
|
||||
"Use {{biometry}}": "Sử dụng {{biometry}}",
|
||||
"Unlock with {{biometry}}": "Mở khóa bằng {{biometry}}",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "Yêu cầu mã PIN (và sinh trắc học, nếu có) để mở Readest",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "sinh trắc học"
|
||||
}
|
||||
|
||||
@@ -1733,5 +1733,12 @@
|
||||
"Erase the book and all its data, including reading progress": "删除该书籍及其所有数据,包括阅读进度",
|
||||
"More Actions": "更多操作",
|
||||
"Sign in and make the book available to share it": "登录并使书籍可用后即可分享",
|
||||
"Download the book to export it": "下载书籍后即可导出"
|
||||
"Download the book to export it": "下载书籍后即可导出",
|
||||
"Unlock Readest": "解锁 Readest",
|
||||
"Use {{biometry}}": "使用{{biometry}}",
|
||||
"Unlock with {{biometry}}": "通过{{biometry}}解锁",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "打开 Readest 需要 PIN 码(如可用,也支持生物识别)",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "生物识别"
|
||||
}
|
||||
|
||||
@@ -1733,5 +1733,12 @@
|
||||
"Erase the book and all its data, including reading progress": "刪除該書籍及其所有資料,包括閱讀進度",
|
||||
"More Actions": "更多操作",
|
||||
"Sign in and make the book available to share it": "登入並使書籍可用後即可分享",
|
||||
"Download the book to export it": "下載書籍後即可匯出"
|
||||
"Download the book to export it": "下載書籍後即可匯出",
|
||||
"Unlock Readest": "解鎖 Readest",
|
||||
"Use {{biometry}}": "使用{{biometry}}",
|
||||
"Unlock with {{biometry}}": "透過{{biometry}}解鎖",
|
||||
"Require a PIN (and biometrics, if available) to open Readest": "開啟 Readest 需要 PIN 碼(如可用,亦支援生物辨識)",
|
||||
"Face ID": "Face ID",
|
||||
"Touch ID": "Touch ID",
|
||||
"biometrics": "生物辨識"
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ tauri-plugin-opener = "2"
|
||||
tauri-plugin-deep-link = "2"
|
||||
tauri-plugin-sign-in-with-apple = "1.0.2"
|
||||
tauri-plugin-haptics = "2"
|
||||
tauri-plugin-biometric = "2"
|
||||
tauri-plugin-persisted-scope = "2"
|
||||
tauri-plugin-native-bridge = { path = "./plugins/tauri-plugin-native-bridge" }
|
||||
tauri-plugin-native-tts = { path = "./plugins/tauri-plugin-native-tts" }
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
<dict>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>Readest uses Face ID to unlock the app.</string>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<false/>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
|
||||
@@ -190,6 +190,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"biometric:default",
|
||||
"haptics:allow-vibrate",
|
||||
"haptics:allow-impact-feedback",
|
||||
"haptics:allow-notification-feedback",
|
||||
|
||||
@@ -359,6 +359,9 @@ pub fn run() {
|
||||
#[cfg(any(target_os = "ios", target_os = "android"))]
|
||||
let builder = builder.plugin(tauri_plugin_haptics::init());
|
||||
|
||||
#[cfg(any(target_os = "ios", target_os = "android"))]
|
||||
let builder = builder.plugin(tauri_plugin_biometric::init());
|
||||
|
||||
#[cfg(feature = "webdriver")]
|
||||
let builder = builder.plugin(tauri_plugin_webdriver::init());
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { render, screen, cleanup, waitFor } from '@testing-library/react';
|
||||
|
||||
const unlockMock = vi.fn();
|
||||
const authenticateWithBiometricsMock = vi.fn();
|
||||
const getBiometricStatusMock = vi.fn();
|
||||
let isSupported = true;
|
||||
let biometricUnlockEnabled = true;
|
||||
let isMobileApp = true;
|
||||
|
||||
vi.mock('@/services/biometric', async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import('@/services/biometric')>('@/services/biometric');
|
||||
return {
|
||||
...actual,
|
||||
isBiometricSupported: () => isSupported,
|
||||
getBiometricStatus: () => getBiometricStatusMock(),
|
||||
authenticateWithBiometrics: (...a: unknown[]) => authenticateWithBiometricsMock(...a),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@/context/EnvContext', () => ({
|
||||
useEnv: () => ({ appService: { isMobile: isMobileApp, isMobileApp } }),
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/useTranslation', () => ({ useTranslation: () => (k: string) => k }));
|
||||
|
||||
vi.mock('@/store/appLockStore', () => ({
|
||||
useAppLockStore: () => ({
|
||||
pinHash: 'h',
|
||||
pinSalt: 's',
|
||||
unlock: unlockMock,
|
||||
biometricUnlockEnabled,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/components/PinInput', () => ({
|
||||
__esModule: true,
|
||||
default: ({ ariaLabel }: { ariaLabel: string }) => <input aria-label={ariaLabel} />,
|
||||
}));
|
||||
|
||||
import AppLockScreen from '@/components/AppLockScreen';
|
||||
|
||||
beforeEach(() => {
|
||||
unlockMock.mockReset();
|
||||
authenticateWithBiometricsMock.mockReset();
|
||||
getBiometricStatusMock.mockReset();
|
||||
getBiometricStatusMock.mockResolvedValue({ available: true, biometryType: 2 });
|
||||
isSupported = true;
|
||||
biometricUnlockEnabled = true;
|
||||
isMobileApp = true;
|
||||
});
|
||||
afterEach(cleanup);
|
||||
|
||||
describe('AppLockScreen biometric gate', () => {
|
||||
it('auto-unlocks when biometric authentication succeeds on mount', async () => {
|
||||
authenticateWithBiometricsMock.mockResolvedValue(true);
|
||||
render(<AppLockScreen />);
|
||||
await waitFor(() => expect(unlockMock).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
it('shows a retry button and stays locked when biometric fails', async () => {
|
||||
authenticateWithBiometricsMock.mockResolvedValue(false);
|
||||
render(<AppLockScreen />);
|
||||
await waitFor(() => expect(authenticateWithBiometricsMock).toHaveBeenCalledTimes(1));
|
||||
expect(unlockMock).not.toHaveBeenCalled();
|
||||
expect(screen.getByRole('button', { name: /Use/ })).toBeTruthy();
|
||||
expect(screen.getByLabelText('PIN code')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('never calls biometric when unsupported (desktop/web)', async () => {
|
||||
isSupported = false;
|
||||
render(<AppLockScreen />);
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
expect(getBiometricStatusMock).not.toHaveBeenCalled();
|
||||
expect(authenticateWithBiometricsMock).not.toHaveBeenCalled();
|
||||
expect(screen.queryByRole('button', { name: /Use/ })).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
||||
|
||||
const authenticateMock = vi.fn();
|
||||
const checkStatusMock = vi.fn();
|
||||
|
||||
vi.mock('@tauri-apps/plugin-biometric', () => ({
|
||||
authenticate: (...args: unknown[]) => authenticateMock(...args),
|
||||
checkStatus: (...args: unknown[]) => checkStatusMock(...args),
|
||||
BiometryType: { None: 0, TouchID: 1, FaceID: 2, Iris: 3 },
|
||||
}));
|
||||
|
||||
import { BiometryType } from '@tauri-apps/plugin-biometric';
|
||||
import {
|
||||
authenticateWithBiometrics,
|
||||
defaultBiometricUnlockOnPinSet,
|
||||
getBiometricStatus,
|
||||
getBiometryLabelKey,
|
||||
isBiometricSupported,
|
||||
shouldAttemptBiometricUnlock,
|
||||
} from '@/services/biometric';
|
||||
|
||||
const mobile = (over: Partial<{ isIOSApp: boolean; isAndroidApp: boolean }> = {}) =>
|
||||
({ isIOSApp: false, isAndroidApp: true, ...over }) as never;
|
||||
|
||||
beforeEach(() => {
|
||||
authenticateMock.mockReset();
|
||||
checkStatusMock.mockReset();
|
||||
});
|
||||
|
||||
describe('isBiometricSupported', () => {
|
||||
test('true on iOS/Android app, false otherwise and for null', () => {
|
||||
expect(isBiometricSupported(mobile({ isIOSApp: true, isAndroidApp: false }))).toBe(true);
|
||||
expect(isBiometricSupported(mobile({ isAndroidApp: true }))).toBe(true);
|
||||
expect(isBiometricSupported({ isIOSApp: false, isAndroidApp: false } as never)).toBe(false);
|
||||
expect(isBiometricSupported(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldAttemptBiometricUnlock', () => {
|
||||
test('requires all three conditions', () => {
|
||||
expect(
|
||||
shouldAttemptBiometricUnlock({
|
||||
isMobileApp: true,
|
||||
biometricUnlockEnabled: true,
|
||||
available: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldAttemptBiometricUnlock({
|
||||
isMobileApp: false,
|
||||
biometricUnlockEnabled: true,
|
||||
available: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldAttemptBiometricUnlock({
|
||||
isMobileApp: true,
|
||||
biometricUnlockEnabled: false,
|
||||
available: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldAttemptBiometricUnlock({
|
||||
isMobileApp: true,
|
||||
biometricUnlockEnabled: true,
|
||||
available: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defaultBiometricUnlockOnPinSet', () => {
|
||||
test('on only when mobile and available', () => {
|
||||
expect(defaultBiometricUnlockOnPinSet({ isMobileApp: true, available: true })).toBe(true);
|
||||
expect(defaultBiometricUnlockOnPinSet({ isMobileApp: true, available: false })).toBe(false);
|
||||
expect(defaultBiometricUnlockOnPinSet({ isMobileApp: false, available: true })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBiometryLabelKey', () => {
|
||||
test('maps biometry type to a label key', () => {
|
||||
expect(getBiometryLabelKey(BiometryType.FaceID)).toBe('Face ID');
|
||||
expect(getBiometryLabelKey(BiometryType.TouchID)).toBe('Touch ID');
|
||||
expect(getBiometryLabelKey(BiometryType.None)).toBe('biometrics');
|
||||
expect(getBiometryLabelKey(BiometryType.Iris)).toBe('biometrics');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBiometricStatus', () => {
|
||||
test('returns availability from checkStatus', async () => {
|
||||
checkStatusMock.mockResolvedValue({ isAvailable: true, biometryType: BiometryType.FaceID });
|
||||
await expect(getBiometricStatus()).resolves.toEqual({
|
||||
available: true,
|
||||
biometryType: BiometryType.FaceID,
|
||||
});
|
||||
});
|
||||
|
||||
test('returns available:false when checkStatus resolves with isAvailable false', async () => {
|
||||
checkStatusMock.mockResolvedValue({ isAvailable: false, biometryType: BiometryType.None });
|
||||
await expect(getBiometricStatus()).resolves.toEqual({
|
||||
available: false,
|
||||
biometryType: BiometryType.None,
|
||||
});
|
||||
});
|
||||
|
||||
test('returns unavailable when checkStatus throws', async () => {
|
||||
checkStatusMock.mockRejectedValue(new Error('no plugin'));
|
||||
await expect(getBiometricStatus()).resolves.toEqual({
|
||||
available: false,
|
||||
biometryType: BiometryType.None,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('authenticateWithBiometrics', () => {
|
||||
test('true when authenticate resolves', async () => {
|
||||
authenticateMock.mockResolvedValue(undefined);
|
||||
await expect(authenticateWithBiometrics('Unlock')).resolves.toBe(true);
|
||||
expect(authenticateMock).toHaveBeenCalledWith(
|
||||
'Unlock',
|
||||
expect.objectContaining({ allowDeviceCredential: false }),
|
||||
);
|
||||
});
|
||||
|
||||
test('false when authenticate throws (cancel/fail)', async () => {
|
||||
authenticateMock.mockRejectedValue(new Error('userCancel'));
|
||||
await expect(authenticateWithBiometrics('Unlock')).resolves.toBe(false);
|
||||
expect(authenticateMock).toHaveBeenCalledWith(
|
||||
'Unlock',
|
||||
expect.objectContaining({ allowDeviceCredential: false }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { PiUserCircle, PiUserCircleCheck, PiGear } from 'react-icons/pi';
|
||||
import { PiSun, PiMoon } from 'react-icons/pi';
|
||||
@@ -26,6 +26,11 @@ import { setAboutDialogVisible } from '@/components/AboutWindow';
|
||||
import { setMigrateDataDirDialogVisible } from '@/app/library/components/MigrateDataWindow';
|
||||
import { requestStoragePermission } from '@/utils/permission';
|
||||
import { saveSysSettings } from '@/helpers/settings';
|
||||
import {
|
||||
getBiometricStatus,
|
||||
getBiometryLabelKey,
|
||||
isBiometricSupported,
|
||||
} from '@/services/biometric';
|
||||
import { selectDirectory } from '@/utils/bridge';
|
||||
import dayjs from 'dayjs';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
@@ -69,6 +74,26 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ onPullLibrary, setIsDropdow
|
||||
const [refreshMetadataProgress, setRefreshMetadataProgress] = useState('');
|
||||
const { openDialog: openAppLockDialogInStore } = useAppLockStore();
|
||||
const isPinEnabled = !!settings.pinCodeEnabled;
|
||||
const [biometricAvailable, setBiometricAvailable] = useState(false);
|
||||
const [biometryLabelKey, setBiometryLabelKey] = useState('');
|
||||
const showBiometricToggle = !!appService?.isMobileApp && isPinEnabled && biometricAvailable;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isBiometricSupported(appService) || !isPinEnabled) return;
|
||||
let cancelled = false;
|
||||
void getBiometricStatus().then(({ available, biometryType }) => {
|
||||
if (cancelled) return;
|
||||
setBiometricAvailable(available);
|
||||
setBiometryLabelKey(getBiometryLabelKey(biometryType));
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [appService, isPinEnabled]);
|
||||
|
||||
const toggleBiometricUnlock = () => {
|
||||
void saveSysSettings(envConfig, 'biometricUnlockEnabled', !settings.biometricUnlockEnabled);
|
||||
};
|
||||
|
||||
const openAppLockDialog = (mode: AppLockDialogMode) => {
|
||||
openAppLockDialogInStore(mode);
|
||||
@@ -417,7 +442,11 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ onPullLibrary, setIsDropdow
|
||||
{!isPinEnabled && (
|
||||
<MenuItem
|
||||
label={_('Set PIN…')}
|
||||
tooltip={_('Require a 4-digit PIN to open Readest')}
|
||||
tooltip={
|
||||
appService?.isMobileApp
|
||||
? _('Require a PIN (and biometrics, if available) to open Readest')
|
||||
: _('Require a 4-digit PIN to open Readest')
|
||||
}
|
||||
onClick={() => openAppLockDialog('set')}
|
||||
/>
|
||||
)}
|
||||
@@ -427,6 +456,13 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ onPullLibrary, setIsDropdow
|
||||
{isPinEnabled && (
|
||||
<MenuItem label={_('Disable PIN…')} onClick={() => openAppLockDialog('disable')} />
|
||||
)}
|
||||
{showBiometricToggle && (
|
||||
<MenuItem
|
||||
label={_('Unlock with {{biometry}}', { biometry: _(biometryLabelKey) })}
|
||||
toggled={!!settings.biometricUnlockEnabled}
|
||||
onClick={toggleBiometricUnlock}
|
||||
/>
|
||||
)}
|
||||
{appService?.isAndroidApp && appService?.distChannel !== 'playstore' && (
|
||||
<MenuItem
|
||||
label={_('Save Book Cover')}
|
||||
|
||||
@@ -8,16 +8,62 @@ import { useEnv } from '@/context/EnvContext';
|
||||
import { PIN_LENGTH, verifyPin } from '@/libs/crypto/applock';
|
||||
import { useAppLockStore } from '@/store/appLockStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import {
|
||||
authenticateWithBiometrics,
|
||||
getBiometricStatus,
|
||||
getBiometryLabelKey,
|
||||
isBiometricSupported,
|
||||
shouldAttemptBiometricUnlock,
|
||||
} from '@/services/biometric';
|
||||
|
||||
export default function AppLockScreen() {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
const { pinHash, pinSalt, unlock } = useAppLockStore();
|
||||
const autoFocusEnabled = !appService?.isMobile;
|
||||
const { pinHash, pinSalt, unlock, biometricUnlockEnabled } = useAppLockStore();
|
||||
const [biometryLabel, setBiometryLabel] = useState('');
|
||||
const [pin, setPin] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [shaking, setShaking] = useState(false);
|
||||
const [kbInset, setKbInset] = useState(0);
|
||||
const biometricAttemptedRef = useRef(false);
|
||||
const biometricInFlightRef = useRef(false);
|
||||
const autoFocusEnabled = !appService?.isMobile;
|
||||
|
||||
const runBiometric = async () => {
|
||||
if (biometricInFlightRef.current) return;
|
||||
biometricInFlightRef.current = true;
|
||||
try {
|
||||
const ok = await authenticateWithBiometrics(_('Unlock Readest'));
|
||||
if (ok) unlock();
|
||||
} finally {
|
||||
biometricInFlightRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isBiometricSupported(appService)) return;
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const { available, biometryType } = await getBiometricStatus();
|
||||
if (cancelled) return;
|
||||
if (
|
||||
!shouldAttemptBiometricUnlock({
|
||||
isMobileApp: !!appService?.isMobileApp,
|
||||
biometricUnlockEnabled,
|
||||
available,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setBiometryLabel(_(getBiometryLabelKey(biometryType)));
|
||||
if (biometricAttemptedRef.current) return;
|
||||
biometricAttemptedRef.current = true;
|
||||
await runBiometric();
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [appService]);
|
||||
|
||||
useEffect(() => {
|
||||
const vv = window.visualViewport;
|
||||
@@ -102,6 +148,20 @@ export default function AppLockScreen() {
|
||||
{error || ' '}
|
||||
</p>
|
||||
|
||||
{biometryLabel && (
|
||||
<button
|
||||
type='button'
|
||||
onClick={runBiometric}
|
||||
className={clsx(
|
||||
'eink-bordered',
|
||||
'mt-2 h-10 rounded-lg px-4 text-sm font-medium',
|
||||
'text-base-content hover:bg-base-200 transition-colors duration-150',
|
||||
)}
|
||||
>
|
||||
{_('Use {{biometry}}', { biometry: biometryLabel })}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<p className='text-base-content/40 mt-10 text-xs leading-relaxed'>
|
||||
{_(
|
||||
"Forgetting your PIN locks you out of this device. You'll need to clear the app's data to reset it.",
|
||||
|
||||
@@ -169,6 +169,7 @@ const Providers = ({ children }: { children: React.ReactNode }) => {
|
||||
enabled: !!settings.pinCodeEnabled,
|
||||
hash: settings.pinCodeHash,
|
||||
salt: settings.pinCodeSalt,
|
||||
biometricUnlockEnabled: !!settings.biometricUnlockEnabled,
|
||||
});
|
||||
// Subscribe the bundled-settings publisher to settingsStore
|
||||
// changes, AFTER priming the publish snapshot from the just-
|
||||
|
||||
@@ -9,6 +9,11 @@ import { useEnv } from '@/context/EnvContext';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { saveSysSettings } from '@/helpers/settings';
|
||||
import { PIN_LENGTH, generatePinSalt, hashPin, isValidPin, verifyPin } from '@/libs/crypto/applock';
|
||||
import {
|
||||
defaultBiometricUnlockOnPinSet,
|
||||
getBiometricStatus,
|
||||
isBiometricSupported,
|
||||
} from '@/services/biometric';
|
||||
import { useAppLockStore } from '@/store/appLockStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
|
||||
@@ -23,7 +28,7 @@ const fieldLabelClass = 'text-base-content/70 text-xs font-medium tracking-wide'
|
||||
*/
|
||||
export default function AppLockDialog() {
|
||||
const _ = useTranslation();
|
||||
const { envConfig } = useEnv();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
const {
|
||||
pinHash,
|
||||
@@ -95,6 +100,14 @@ export default function AppLockDialog() {
|
||||
await saveSysSettings(envConfig, 'pinCodeSalt', salt);
|
||||
await saveSysSettings(envConfig, 'pinCodeHash', hash);
|
||||
await saveSysSettings(envConfig, 'pinCodeEnabled', true);
|
||||
if (isBiometricSupported(appService)) {
|
||||
const { available } = await getBiometricStatus();
|
||||
await saveSysSettings(
|
||||
envConfig,
|
||||
'biometricUnlockEnabled',
|
||||
defaultBiometricUnlockOnPinSet({ isMobileApp: !!appService?.isMobileApp, available }),
|
||||
);
|
||||
}
|
||||
setStorePin(hash, salt);
|
||||
closeDialog();
|
||||
} finally {
|
||||
@@ -162,6 +175,7 @@ export default function AppLockDialog() {
|
||||
await saveSysSettings(envConfig, 'pinCodeEnabled', false);
|
||||
await saveSysSettings(envConfig, 'pinCodeHash', undefined);
|
||||
await saveSysSettings(envConfig, 'pinCodeSalt', undefined);
|
||||
await saveSysSettings(envConfig, 'biometricUnlockEnabled', undefined);
|
||||
clearPin();
|
||||
closeDialog();
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { authenticate, checkStatus, BiometryType } from '@tauri-apps/plugin-biometric';
|
||||
|
||||
import type { AppService } from '@/types/system';
|
||||
import { stubTranslation as _ } from '@/utils/misc';
|
||||
|
||||
/**
|
||||
* Everything biometric lives behind this wrapper so the rest of the app
|
||||
* never imports `@tauri-apps/plugin-biometric` directly. Off mobile-Tauri
|
||||
* the plugin commands are never reached (callers gate on
|
||||
* `isBiometricSupported`), and the async wrappers swallow errors so a
|
||||
* missing plugin can never throw into the UI.
|
||||
*/
|
||||
|
||||
export const isBiometricSupported = (appService: AppService | null): boolean =>
|
||||
!!(appService?.isIOSApp || appService?.isAndroidApp);
|
||||
|
||||
export const getBiometricStatus = async (): Promise<{
|
||||
available: boolean;
|
||||
biometryType: BiometryType;
|
||||
}> => {
|
||||
try {
|
||||
const status = await checkStatus();
|
||||
return { available: status.isAvailable, biometryType: status.biometryType };
|
||||
} catch {
|
||||
return { available: false, biometryType: BiometryType.None };
|
||||
}
|
||||
};
|
||||
|
||||
export const authenticateWithBiometrics = async (reason: string): Promise<boolean> => {
|
||||
try {
|
||||
await authenticate(reason, { allowDeviceCredential: false });
|
||||
return true;
|
||||
} catch {
|
||||
// Cancel, no-match, lockout, or unavailable — fall back to the PIN.
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const shouldAttemptBiometricUnlock = (opts: {
|
||||
isMobileApp: boolean;
|
||||
biometricUnlockEnabled: boolean;
|
||||
available: boolean;
|
||||
}): boolean => opts.isMobileApp && opts.biometricUnlockEnabled && opts.available;
|
||||
|
||||
export const defaultBiometricUnlockOnPinSet = (opts: {
|
||||
isMobileApp: boolean;
|
||||
available: boolean;
|
||||
}): boolean => opts.isMobileApp && opts.available;
|
||||
|
||||
/**
|
||||
* i18n key for the biometry name. Returned key is fed back through `_()`
|
||||
* in the component (key-as-content i18n); `stubTranslation` here just
|
||||
* registers the strings for extraction.
|
||||
*/
|
||||
export const getBiometryLabelKey = (biometryType: BiometryType): string => {
|
||||
if (biometryType === BiometryType.FaceID) return _('Face ID');
|
||||
if (biometryType === BiometryType.TouchID) return _('Touch ID');
|
||||
return _('biometrics');
|
||||
};
|
||||
@@ -23,8 +23,20 @@ interface AppLockState {
|
||||
pinHash: string | null;
|
||||
pinSalt: string | null;
|
||||
|
||||
/**
|
||||
* Startup snapshot of `SystemSettings.biometricUnlockEnabled` (mobile-only).
|
||||
* Threaded through `initialize` so `AppLockScreen` never races the page-level
|
||||
* settingsStore seed.
|
||||
*/
|
||||
biometricUnlockEnabled: boolean;
|
||||
|
||||
/** Called once from `Providers` after `loadSettings` resolves. */
|
||||
initialize: (config: { enabled: boolean; hash?: string; salt?: string }) => void;
|
||||
initialize: (config: {
|
||||
enabled: boolean;
|
||||
hash?: string;
|
||||
salt?: string;
|
||||
biometricUnlockEnabled?: boolean;
|
||||
}) => void;
|
||||
|
||||
/** Called by `<AppLockScreen />` after a verified PIN entry. */
|
||||
unlock: () => void;
|
||||
@@ -53,12 +65,14 @@ export const useAppLockStore = create<AppLockState>((set) => ({
|
||||
isUnlocked: true,
|
||||
pinHash: null,
|
||||
pinSalt: null,
|
||||
initialize: ({ enabled, hash, salt }) =>
|
||||
biometricUnlockEnabled: false,
|
||||
initialize: ({ enabled, hash, salt, biometricUnlockEnabled }) =>
|
||||
set({
|
||||
isInitialized: true,
|
||||
isUnlocked: !enabled,
|
||||
pinHash: hash ?? null,
|
||||
pinSalt: salt ?? null,
|
||||
biometricUnlockEnabled: !!biometricUnlockEnabled,
|
||||
}),
|
||||
unlock: () => set({ isUnlocked: true }),
|
||||
lock: () => set({ isUnlocked: false }),
|
||||
|
||||
@@ -346,6 +346,14 @@ export interface SystemSettings {
|
||||
pinCodeEnabled?: boolean;
|
||||
pinCodeHash?: string;
|
||||
pinCodeSalt?: string;
|
||||
/**
|
||||
* Mobile-only. When true AND a PIN lock is configured AND the device
|
||||
* has enrolled biometrics, the app-lock screen prompts for biometrics
|
||||
* (fingerprint / Face ID) first and falls back to the PIN. No effect on
|
||||
* desktop/web (no biometric plugin). `undefined` is treated as `false`
|
||||
* so existing PIN users are never silently switched to biometric.
|
||||
*/
|
||||
biometricUnlockEnabled?: boolean;
|
||||
|
||||
kosync: KOSyncSettings;
|
||||
readwise: ReadwiseSettings;
|
||||
|
||||
Generated
+10
@@ -180,6 +180,9 @@ importers:
|
||||
'@tauri-apps/api':
|
||||
specifier: 2.10.1
|
||||
version: 2.10.1
|
||||
'@tauri-apps/plugin-biometric':
|
||||
specifier: ^2.3.2
|
||||
version: 2.3.2
|
||||
'@tauri-apps/plugin-cli':
|
||||
specifier: ^2.4.1
|
||||
version: 2.4.1
|
||||
@@ -3304,6 +3307,9 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
hasBin: true
|
||||
|
||||
'@tauri-apps/plugin-biometric@2.3.2':
|
||||
resolution: {integrity: sha512-RFydYGKEJ6m4UWdcPsO+vJQUb3JyyfFeNoZSPDs0uWjvwHBs03xey5wWO+uowmFFy03JAff1l2idQAwuycO8Uw==}
|
||||
|
||||
'@tauri-apps/plugin-cli@2.4.1':
|
||||
resolution: {integrity: sha512-8JXofQFI5cmiGolh1PlU4hzE2YJgrgB1lyaztyBYiiMCy13luVxBXaXChYPeqMkUo46J1UadxvYdjRjj0E8zaw==}
|
||||
|
||||
@@ -11655,6 +11661,10 @@ snapshots:
|
||||
'@tauri-apps/cli-win32-ia32-msvc': 2.10.1
|
||||
'@tauri-apps/cli-win32-x64-msvc': 2.10.1
|
||||
|
||||
'@tauri-apps/plugin-biometric@2.3.2':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.10.1
|
||||
|
||||
'@tauri-apps/plugin-cli@2.4.1':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.10.1
|
||||
|
||||
Reference in New Issue
Block a user