From 6c7c86f346a3e2fab031276de6637ada7a844f53 Mon Sep 17 00:00:00 2001
From: Huang Xin
Date: Fri, 19 Jun 2026 00:39:03 +0800
Subject: [PATCH] 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
* 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
* 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
* feat(applock): mobile biometric toggle + default-on at PIN setup
Co-Authored-By: Claude Sonnet 4.6
* 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
* build(applock): pin tauri-plugin-biometric in Cargo.lock
Co-Authored-By: Claude Opus 4.8 (1M context)
---------
Co-authored-by: Claude Sonnet 4.6
---
Cargo.lock | 16 +++
apps/readest-app/package.json | 1 +
.../public/locales/ar/translation.json | 9 +-
.../public/locales/bn/translation.json | 9 +-
.../public/locales/bo/translation.json | 9 +-
.../public/locales/de/translation.json | 9 +-
.../public/locales/el/translation.json | 9 +-
.../public/locales/es/translation.json | 9 +-
.../public/locales/fa/translation.json | 9 +-
.../public/locales/fr/translation.json | 9 +-
.../public/locales/he/translation.json | 9 +-
.../public/locales/hi/translation.json | 9 +-
.../public/locales/hu/translation.json | 9 +-
.../public/locales/id/translation.json | 9 +-
.../public/locales/it/translation.json | 9 +-
.../public/locales/ja/translation.json | 9 +-
.../public/locales/ko/translation.json | 9 +-
.../public/locales/ms/translation.json | 9 +-
.../public/locales/nl/translation.json | 9 +-
.../public/locales/pl/translation.json | 9 +-
.../public/locales/pt-BR/translation.json | 9 +-
.../public/locales/pt/translation.json | 9 +-
.../public/locales/ro/translation.json | 9 +-
.../public/locales/ru/translation.json | 9 +-
.../public/locales/si/translation.json | 9 +-
.../public/locales/sl/translation.json | 9 +-
.../public/locales/sv/translation.json | 9 +-
.../public/locales/ta/translation.json | 9 +-
.../public/locales/th/translation.json | 9 +-
.../public/locales/tr/translation.json | 9 +-
.../public/locales/uk/translation.json | 9 +-
.../public/locales/uz/translation.json | 9 +-
.../public/locales/vi/translation.json | 9 +-
.../public/locales/zh-CN/translation.json | 9 +-
.../public/locales/zh-TW/translation.json | 9 +-
apps/readest-app/src-tauri/Cargo.toml | 1 +
apps/readest-app/src-tauri/Info.plist | 2 +
.../src-tauri/capabilities/default.json | 1 +
apps/readest-app/src-tauri/src/lib.rs | 3 +
.../components/AppLockScreen.test.tsx | 79 +++++++++++
.../src/__tests__/services/biometric.test.ts | 133 ++++++++++++++++++
.../app/library/components/SettingsMenu.tsx | 40 +++++-
.../src/components/AppLockScreen.tsx | 64 ++++++++-
apps/readest-app/src/components/Providers.tsx | 1 +
.../src/components/settings/AppLockDialog.tsx | 16 ++-
apps/readest-app/src/services/biometric.ts | 59 ++++++++
apps/readest-app/src/store/appLockStore.ts | 18 ++-
apps/readest-app/src/types/settings.ts | 8 ++
pnpm-lock.yaml | 10 ++
49 files changed, 709 insertions(+), 40 deletions(-)
create mode 100644 apps/readest-app/src/__tests__/components/AppLockScreen.test.tsx
create mode 100644 apps/readest-app/src/__tests__/services/biometric.test.ts
create mode 100644 apps/readest-app/src/services/biometric.ts
diff --git a/Cargo.lock b/Cargo.lock
index 06162c0d..49153bd1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/apps/readest-app/package.json b/apps/readest-app/package.json
index a3d514c5..f9265517 100644
--- a/apps/readest-app/package.json
+++ b/apps/readest-app/package.json
@@ -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",
diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json
index 8c18115e..a159e174 100644
--- a/apps/readest-app/public/locales/ar/translation.json
+++ b/apps/readest-app/public/locales/ar/translation.json
@@ -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": "البيومترية"
}
diff --git a/apps/readest-app/public/locales/bn/translation.json b/apps/readest-app/public/locales/bn/translation.json
index 882fbe16..9938d382 100644
--- a/apps/readest-app/public/locales/bn/translation.json
+++ b/apps/readest-app/public/locales/bn/translation.json
@@ -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": "বায়োমেট্রিক্স"
}
diff --git a/apps/readest-app/public/locales/bo/translation.json b/apps/readest-app/public/locales/bo/translation.json
index 20ef0a76..7052fa15 100644
--- a/apps/readest-app/public/locales/bo/translation.json
+++ b/apps/readest-app/public/locales/bo/translation.json
@@ -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": "ལུས་རྟགས་ངོ་སྤྲོད།"
}
diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json
index e16a6d06..119b88c1 100644
--- a/apps/readest-app/public/locales/de/translation.json
+++ b/apps/readest-app/public/locales/de/translation.json
@@ -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"
}
diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json
index bb44eff4..ea889492 100644
--- a/apps/readest-app/public/locales/el/translation.json
+++ b/apps/readest-app/public/locales/el/translation.json
@@ -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": "βιομετρικά"
}
diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json
index a006e5c2..6bded3f3 100644
--- a/apps/readest-app/public/locales/es/translation.json
+++ b/apps/readest-app/public/locales/es/translation.json
@@ -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"
}
diff --git a/apps/readest-app/public/locales/fa/translation.json b/apps/readest-app/public/locales/fa/translation.json
index b447df70..3468ae13 100644
--- a/apps/readest-app/public/locales/fa/translation.json
+++ b/apps/readest-app/public/locales/fa/translation.json
@@ -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": "بیومتریک"
}
diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json
index 0e5eef5e..cfbbe2a3 100644
--- a/apps/readest-app/public/locales/fr/translation.json
+++ b/apps/readest-app/public/locales/fr/translation.json
@@ -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"
}
diff --git a/apps/readest-app/public/locales/he/translation.json b/apps/readest-app/public/locales/he/translation.json
index ccaf7a18..2846c1c7 100644
--- a/apps/readest-app/public/locales/he/translation.json
+++ b/apps/readest-app/public/locales/he/translation.json
@@ -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": "ביומטריה"
}
diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json
index e16f959d..73fe9922 100644
--- a/apps/readest-app/public/locales/hi/translation.json
+++ b/apps/readest-app/public/locales/hi/translation.json
@@ -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": "बायोमेट्रिक्स"
}
diff --git a/apps/readest-app/public/locales/hu/translation.json b/apps/readest-app/public/locales/hu/translation.json
index 6b210a62..2fff1ae7 100644
--- a/apps/readest-app/public/locales/hu/translation.json
+++ b/apps/readest-app/public/locales/hu/translation.json
@@ -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"
}
diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json
index 9c5046ad..91133086 100644
--- a/apps/readest-app/public/locales/id/translation.json
+++ b/apps/readest-app/public/locales/id/translation.json
@@ -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"
}
diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json
index 8e6d6766..42c29559 100644
--- a/apps/readest-app/public/locales/it/translation.json
+++ b/apps/readest-app/public/locales/it/translation.json
@@ -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"
}
diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json
index 14babbb9..87ed0d10 100644
--- a/apps/readest-app/public/locales/ja/translation.json
+++ b/apps/readest-app/public/locales/ja/translation.json
@@ -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": "生体認証"
}
diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json
index defe08a1..0b26f62c 100644
--- a/apps/readest-app/public/locales/ko/translation.json
+++ b/apps/readest-app/public/locales/ko/translation.json
@@ -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": "생체 인증"
}
diff --git a/apps/readest-app/public/locales/ms/translation.json b/apps/readest-app/public/locales/ms/translation.json
index 742bf90d..33c24d38 100644
--- a/apps/readest-app/public/locales/ms/translation.json
+++ b/apps/readest-app/public/locales/ms/translation.json
@@ -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"
}
diff --git a/apps/readest-app/public/locales/nl/translation.json b/apps/readest-app/public/locales/nl/translation.json
index 695b8b86..a48194a4 100644
--- a/apps/readest-app/public/locales/nl/translation.json
+++ b/apps/readest-app/public/locales/nl/translation.json
@@ -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"
}
diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json
index 33a66334..77728cfb 100644
--- a/apps/readest-app/public/locales/pl/translation.json
+++ b/apps/readest-app/public/locales/pl/translation.json
@@ -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"
}
diff --git a/apps/readest-app/public/locales/pt-BR/translation.json b/apps/readest-app/public/locales/pt-BR/translation.json
index 4afe0ade..56968a30 100644
--- a/apps/readest-app/public/locales/pt-BR/translation.json
+++ b/apps/readest-app/public/locales/pt-BR/translation.json
@@ -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"
}
diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json
index be8aaced..95502876 100644
--- a/apps/readest-app/public/locales/pt/translation.json
+++ b/apps/readest-app/public/locales/pt/translation.json
@@ -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"
}
diff --git a/apps/readest-app/public/locales/ro/translation.json b/apps/readest-app/public/locales/ro/translation.json
index f8af5ef8..aeea7cfa 100644
--- a/apps/readest-app/public/locales/ro/translation.json
+++ b/apps/readest-app/public/locales/ro/translation.json
@@ -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"
}
diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json
index 78842e72..4cf46754 100644
--- a/apps/readest-app/public/locales/ru/translation.json
+++ b/apps/readest-app/public/locales/ru/translation.json
@@ -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": "биометрия"
}
diff --git a/apps/readest-app/public/locales/si/translation.json b/apps/readest-app/public/locales/si/translation.json
index 0fdd646e..64734512 100644
--- a/apps/readest-app/public/locales/si/translation.json
+++ b/apps/readest-app/public/locales/si/translation.json
@@ -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": "ජෛව මිතික"
}
diff --git a/apps/readest-app/public/locales/sl/translation.json b/apps/readest-app/public/locales/sl/translation.json
index d893a97a..ee971b20 100644
--- a/apps/readest-app/public/locales/sl/translation.json
+++ b/apps/readest-app/public/locales/sl/translation.json
@@ -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"
}
diff --git a/apps/readest-app/public/locales/sv/translation.json b/apps/readest-app/public/locales/sv/translation.json
index 26b018fe..244668d5 100644
--- a/apps/readest-app/public/locales/sv/translation.json
+++ b/apps/readest-app/public/locales/sv/translation.json
@@ -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"
}
diff --git a/apps/readest-app/public/locales/ta/translation.json b/apps/readest-app/public/locales/ta/translation.json
index e706259a..fbd4eec0 100644
--- a/apps/readest-app/public/locales/ta/translation.json
+++ b/apps/readest-app/public/locales/ta/translation.json
@@ -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": "உயிரியல் அடையாளம்"
}
diff --git a/apps/readest-app/public/locales/th/translation.json b/apps/readest-app/public/locales/th/translation.json
index c1fc8759..89a79a0f 100644
--- a/apps/readest-app/public/locales/th/translation.json
+++ b/apps/readest-app/public/locales/th/translation.json
@@ -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": "ข้อมูลไบโอเมตริก"
}
diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json
index 36f9acea..dd1cd863 100644
--- a/apps/readest-app/public/locales/tr/translation.json
+++ b/apps/readest-app/public/locales/tr/translation.json
@@ -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"
}
diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json
index 0cc5c895..b24345a9 100644
--- a/apps/readest-app/public/locales/uk/translation.json
+++ b/apps/readest-app/public/locales/uk/translation.json
@@ -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": "біометрія"
}
diff --git a/apps/readest-app/public/locales/uz/translation.json b/apps/readest-app/public/locales/uz/translation.json
index 83168645..1768ba94 100644
--- a/apps/readest-app/public/locales/uz/translation.json
+++ b/apps/readest-app/public/locales/uz/translation.json
@@ -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"
}
diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json
index c4612656..79062cb4 100644
--- a/apps/readest-app/public/locales/vi/translation.json
+++ b/apps/readest-app/public/locales/vi/translation.json
@@ -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"
}
diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json
index 820eff27..784d142f 100644
--- a/apps/readest-app/public/locales/zh-CN/translation.json
+++ b/apps/readest-app/public/locales/zh-CN/translation.json
@@ -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": "生物识别"
}
diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json
index c552d740..e0ae7618 100644
--- a/apps/readest-app/public/locales/zh-TW/translation.json
+++ b/apps/readest-app/public/locales/zh-TW/translation.json
@@ -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": "生物辨識"
}
diff --git a/apps/readest-app/src-tauri/Cargo.toml b/apps/readest-app/src-tauri/Cargo.toml
index b3c76e7a..58de1607 100644
--- a/apps/readest-app/src-tauri/Cargo.toml
+++ b/apps/readest-app/src-tauri/Cargo.toml
@@ -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" }
diff --git a/apps/readest-app/src-tauri/Info.plist b/apps/readest-app/src-tauri/Info.plist
index bc00a3e0..555429ea 100644
--- a/apps/readest-app/src-tauri/Info.plist
+++ b/apps/readest-app/src-tauri/Info.plist
@@ -4,6 +4,8 @@
ITSAppUsesNonExemptEncryption
+ NSFaceIDUsageDescription
+ Readest uses Face ID to unlock the app.LSSupportsOpeningDocumentsInPlaceUIViewControllerBasedStatusBarAppearance
diff --git a/apps/readest-app/src-tauri/capabilities/default.json b/apps/readest-app/src-tauri/capabilities/default.json
index 2adb844f..127dd322 100644
--- a/apps/readest-app/src-tauri/capabilities/default.json
+++ b/apps/readest-app/src-tauri/capabilities/default.json
@@ -190,6 +190,7 @@
}
]
},
+ "biometric:default",
"haptics:allow-vibrate",
"haptics:allow-impact-feedback",
"haptics:allow-notification-feedback",
diff --git a/apps/readest-app/src-tauri/src/lib.rs b/apps/readest-app/src-tauri/src/lib.rs
index 07342384..591ee125 100644
--- a/apps/readest-app/src-tauri/src/lib.rs
+++ b/apps/readest-app/src-tauri/src/lib.rs
@@ -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());
diff --git a/apps/readest-app/src/__tests__/components/AppLockScreen.test.tsx b/apps/readest-app/src/__tests__/components/AppLockScreen.test.tsx
new file mode 100644
index 00000000..b600112b
--- /dev/null
+++ b/apps/readest-app/src/__tests__/components/AppLockScreen.test.tsx
@@ -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('@/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 }) => ,
+}));
+
+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();
+ await waitFor(() => expect(unlockMock).toHaveBeenCalledTimes(1));
+ });
+
+ it('shows a retry button and stays locked when biometric fails', async () => {
+ authenticateWithBiometricsMock.mockResolvedValue(false);
+ render();
+ 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();
+ await new Promise((r) => setTimeout(r, 50));
+ expect(getBiometricStatusMock).not.toHaveBeenCalled();
+ expect(authenticateWithBiometricsMock).not.toHaveBeenCalled();
+ expect(screen.queryByRole('button', { name: /Use/ })).toBeNull();
+ });
+});
diff --git a/apps/readest-app/src/__tests__/services/biometric.test.ts b/apps/readest-app/src/__tests__/services/biometric.test.ts
new file mode 100644
index 00000000..ef7145fa
--- /dev/null
+++ b/apps/readest-app/src/__tests__/services/biometric.test.ts
@@ -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 }),
+ );
+ });
+});
diff --git a/apps/readest-app/src/app/library/components/SettingsMenu.tsx b/apps/readest-app/src/app/library/components/SettingsMenu.tsx
index 0df1f6fe..f81846cb 100644
--- a/apps/readest-app/src/app/library/components/SettingsMenu.tsx
+++ b/apps/readest-app/src/app/library/components/SettingsMenu.tsx
@@ -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 = ({ 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 = ({ onPullLibrary, setIsDropdow
{!isPinEnabled && (
+ {biometryLabel && (
+
+ )}
+
{_(
"Forgetting your PIN locks you out of this device. You'll need to clear the app's data to reset it.",
diff --git a/apps/readest-app/src/components/Providers.tsx b/apps/readest-app/src/components/Providers.tsx
index a3ee1320..b789f48d 100644
--- a/apps/readest-app/src/components/Providers.tsx
+++ b/apps/readest-app/src/components/Providers.tsx
@@ -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-
diff --git a/apps/readest-app/src/components/settings/AppLockDialog.tsx b/apps/readest-app/src/components/settings/AppLockDialog.tsx
index 1d6633a1..9bb5c2aa 100644
--- a/apps/readest-app/src/components/settings/AppLockDialog.tsx
+++ b/apps/readest-app/src/components/settings/AppLockDialog.tsx
@@ -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 {
diff --git a/apps/readest-app/src/services/biometric.ts b/apps/readest-app/src/services/biometric.ts
new file mode 100644
index 00000000..4848124f
--- /dev/null
+++ b/apps/readest-app/src/services/biometric.ts
@@ -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 => {
+ 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');
+};
diff --git a/apps/readest-app/src/store/appLockStore.ts b/apps/readest-app/src/store/appLockStore.ts
index 3a4e4ee4..46c456e5 100644
--- a/apps/readest-app/src/store/appLockStore.ts
+++ b/apps/readest-app/src/store/appLockStore.ts
@@ -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 `` after a verified PIN entry. */
unlock: () => void;
@@ -53,12 +65,14 @@ export const useAppLockStore = create((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 }),
diff --git a/apps/readest-app/src/types/settings.ts b/apps/readest-app/src/types/settings.ts
index 384cecfa..c613b8ff 100644
--- a/apps/readest-app/src/types/settings.ts
+++ b/apps/readest-app/src/types/settings.ts
@@ -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;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 938ef26f..64585286 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -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