diff --git a/android/app/build.gradle b/android/app/build.gradle
index d606b22b89..916d2adaa9 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -276,6 +276,7 @@ dependencies {
implementation project(':react-native-securerandom')
implementation project(':react-native-webview-bridge')
implementation project(':react-native-webview')
+ implementation project(':react-native-touch-id')
implementation project(':react-native-config')
implementation project(':react-native-firebase')
implementation project(':react-native-shake')
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 14e2b18647..bc649f4fa7 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -18,6 +18,7 @@
+
diff --git a/android/app/src/main/java/im/status/ethereum/MainApplication.java b/android/app/src/main/java/im/status/ethereum/MainApplication.java
index c6886a07bb..179a52628a 100644
--- a/android/app/src/main/java/im/status/ethereum/MainApplication.java
+++ b/android/app/src/main/java/im/status/ethereum/MainApplication.java
@@ -17,6 +17,7 @@ import com.ocetnik.timer.BackgroundTimerPackage;
import com.reactcommunity.rnlanguages.RNLanguagesPackage;
import com.reactnative.ivpusic.imagepicker.PickerPackage;
import com.rnfs.RNFSPackage;
+import com.rnfingerprint.FingerprintAuthPackage;
import net.rhogan.rnsecurerandom.RNSecureRandomPackage;
@@ -74,7 +75,8 @@ public class MainApplication extends MultiDexApplication implements ReactApplica
new RNCWebViewPackage(),
new ReactNativeConfigPackage(),
new KeychainPackage(),
- new RNShakeEventPackage());
+ new RNShakeEventPackage(),
+ new FingerprintAuthPackage());
}
@Override
diff --git a/android/settings.gradle b/android/settings.gradle
index 41ee4d0294..ca49acc3c5 100644
--- a/android/settings.gradle
+++ b/android/settings.gradle
@@ -38,6 +38,8 @@ include ':react-native-webview-bridge'
project(':react-native-webview-bridge').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview-bridge/android')
include ':react-native-webview'
project(':react-native-webview').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview/android')
+include ':react-native-touch-id'
+project(':react-native-touch-id').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-touch-id/android')
include ':react-native-config'
project(':react-native-config').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-config/android')
include ':react-native-android'
diff --git a/clj-rn.conf.edn b/clj-rn.conf.edn
index 55455b90ff..7d1ea20642 100644
--- a/clj-rn.conf.edn
+++ b/clj-rn.conf.edn
@@ -23,6 +23,7 @@
"react-native-webview-bridge"
"react-native-webview"
"react-native-firebase"
+ "react-native-touch-id"
"homoglyph-finder"
"web3"
"web3-utils"
diff --git a/ios/Podfile b/ios/Podfile
index 5b863508b0..d732cd02de 100644
--- a/ios/Podfile
+++ b/ios/Podfile
@@ -18,6 +18,7 @@ target 'StatusIm' do
pod 'RNKeychain', :path => '../node_modules/react-native-keychain'
pod 'react-native-camera', path: '../node_modules/react-native-camera'
pod 'react-native-webview', path: '../node_modules/react-native-webview'
+ pod 'TouchID', path: '../node_modules/react-native-touch-id'
pod 'SQLCipher', '~>3.0'
pod 'SSZipArchive'
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 80c027c3cf..5b43971f46 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -78,6 +78,8 @@ PODS:
- SQLCipher/standard (3.4.2):
- SQLCipher/common
- SSZipArchive (2.1.4)
+ - TouchID (4.4.1):
+ - React
- yoga (0.59.3.React)
DEPENDENCIES:
@@ -90,6 +92,7 @@ DEPENDENCIES:
- RNKeychain (from `../node_modules/react-native-keychain`)
- SQLCipher (~> 3.0)
- SSZipArchive
+ - TouchID (from `../node_modules/react-native-touch-id`)
- yoga (from `../node_modules/react-native/ReactCommon/yoga`)
SPEC REPOS:
@@ -117,6 +120,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-webview"
RNKeychain:
:path: "../node_modules/react-native-keychain"
+ TouchID:
+ :path: "../node_modules/react-native-touch-id"
yoga:
:path: "../node_modules/react-native/ReactCommon/yoga"
@@ -137,8 +142,9 @@ SPEC CHECKSUMS:
RNKeychain: 627c6095cef215dd3d9804a9a9cf45ab96aa3997
SQLCipher: f9fcf29b2e59ced7defc2a2bdd0ebe79b40d4990
SSZipArchive: 41455d4b8d2b6ab93990820b50dc697c2554a322
+ TouchID: b0640fedb86fa2db2fe1df15b61594ad49e76288
yoga: 128daf064cacaede0c3bb27424b6b4c71052e6cd
-PODFILE CHECKSUM: 1b73a7ab29d939e99e86434864c837afaf55851c
+PODFILE CHECKSUM: 13006106aa0716d54fffebf8dcb42006f0fb8798
COCOAPODS: 1.5.3
diff --git a/ios/StatusIm/Info.plist b/ios/StatusIm/Info.plist
index a6c8f44069..8492a45750 100644
--- a/ios/StatusIm/Info.plist
+++ b/ios/StatusIm/Info.plist
@@ -4,6 +4,8 @@
CFBundleBuildUrl
????
+ NSFaceIDUsageDescription
+ Enabling Face ID allows you quick and secure access to your account.
CFBundleDevelopmentRegion
en
CFBundleDisplayName
diff --git a/mobile_files/package.json.orig b/mobile_files/package.json.orig
index a5b6582f43..9ab8299dd8 100644
--- a/mobile_files/package.json.orig
+++ b/mobile_files/package.json.orig
@@ -60,6 +60,7 @@
"react-native-svg": "^9.2.4",
"react-native-svg-transformer": "^0.12.1",
"react-native-tcp": "git+https://github.com/status-im/react-native-tcp.git#v3.3.0-1-status",
+ "react-native-touch-id": "^4.4.1",
"react-native-udp": "git+https://github.com/status-im/react-native-udp.git#2.3.1-1",
"react-native-webview": "^5.2.1",
"react-native-webview-bridge": "git+https://github.com/status-im/react-native-webview-bridge.git#fix/classnames-colision",
diff --git a/mobile_files/yarn.lock b/mobile_files/yarn.lock
index 876bc1f811..4bb8bd9a12 100644
--- a/mobile_files/yarn.lock
+++ b/mobile_files/yarn.lock
@@ -5635,6 +5635,11 @@ react-native-tab-view@^1.0.0:
process "^0.11.9"
util "^0.10.3"
+react-native-touch-id@^4.4.1:
+ version "4.4.1"
+ resolved "https://registry.yarnpkg.com/react-native-touch-id/-/react-native-touch-id-4.4.1.tgz#8b1bb2d04c30bac36bb9696d2d723e719c4a8b08"
+ integrity sha512-1jTl8fC+0fxvqegy/XXTyo6vMvPhjzkoDdaqoYZx0OH8AT250NuXnNPyKktvigIcys3+2acciqOeaCall7lrvg==
+
"react-native-udp@git+https://github.com/status-im/react-native-udp.git#2.3.1-1":
version "2.3.1"
resolved "git+https://github.com/status-im/react-native-udp.git#6e9a817326208f6ca36fa42b20f5e530c1b39d37"
diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java
index ab49ff5199..d37453e180 100644
--- a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java
+++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java
@@ -1122,6 +1122,10 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
HashMap constants = new HashMap();
constants.put("is24Hour", this.is24Hour());
+ constants.put("model", Build.MODEL);
+ constants.put("brand", Build.BRAND);
+ constants.put("buildId", Build.ID);
+ constants.put("deviceId", Build.BOARD);
return constants;
}
diff --git a/modules/react-native-status/ios/RCTStatus/RCTStatus.h b/modules/react-native-status/ios/RCTStatus/RCTStatus.h
index f09d7fe841..3f3873c8d8 100644
--- a/modules/react-native-status/ios/RCTStatus/RCTStatus.h
+++ b/modules/react-native-status/ios/RCTStatus/RCTStatus.h
@@ -1,3 +1,4 @@
+#import
#import
#import
#import "Statusgo/Statusgo.h"
diff --git a/modules/react-native-status/ios/RCTStatus/RCTStatus.m b/modules/react-native-status/ios/RCTStatus/RCTStatus.m
index 207abc6203..77c3d79317 100644
--- a/modules/react-native-status/ios/RCTStatus/RCTStatus.m
+++ b/modules/react-native-status/ios/RCTStatus/RCTStatus.m
@@ -548,16 +548,63 @@ RCT_EXPORT_METHOD(setBlankPreviewFlag:(BOOL *)newValue)
[userDefaults synchronize];
}
+//// deviceinfo
+
- (bool) is24Hour
{
NSString *format = [NSDateFormatter dateFormatFromTemplate:@"j" options:0 locale:[NSLocale currentLocale]];
return ([format rangeOfString:@"a"].location == NSNotFound);
}
+- (NSString *)getBuildId {
+ return @"not available";
+}
+
+- (NSString*) deviceId
+{
+ struct utsname systemInfo;
+
+ uname(&systemInfo);
+
+ NSString* deviceId = [NSString stringWithCString:systemInfo.machine
+ encoding:NSUTF8StringEncoding];
+
+ if ([deviceId isEqualToString:@"i386"] || [deviceId isEqualToString:@"x86_64"] ) {
+ deviceId = [NSString stringWithFormat:@"%s", getenv("SIMULATOR_MODEL_IDENTIFIER")];
+ }
+
+ return deviceId;
+}
+
+- (NSString*) deviceName
+{
+
+ NSString* deviceName = nil;
+
+ if ([self.deviceId rangeOfString:@"iPod"].location != NSNotFound) {
+ deviceName = @"iPod Touch";
+ }
+ else if([self.deviceId rangeOfString:@"iPad"].location != NSNotFound) {
+ deviceName = @"iPad";
+ }
+ else if([self.deviceId rangeOfString:@"iPhone"].location != NSNotFound){
+ deviceName = @"iPhone";
+ }
+ else if([self.deviceId rangeOfString:@"AppleTV"].location != NSNotFound){
+ deviceName = @"Apple TV";
+ }
+
+ return deviceName;
+}
+
- (NSDictionary *)constantsToExport
{
return @{
@"is24Hour": @(self.is24Hour),
+ @"model": self.deviceName ?: [NSNull null],
+ @"brand": @"Apple",
+ @"buildId": [self getBuildId],
+ @"deviceId": self.deviceId ?: [NSNull null],
};
}
diff --git a/react-native/src/desktop/status_im/react_native/js_dependencies.cljs b/react-native/src/desktop/status_im/react_native/js_dependencies.cljs
index ae6eb8d308..e0f847d545 100644
--- a/react-native/src/desktop/status_im/react_native/js_dependencies.cljs
+++ b/react-native/src/desktop/status_im/react_native/js_dependencies.cljs
@@ -21,6 +21,7 @@
(def desktop-config (js/require "react-native-desktop-config"))
(def desktop-shortcuts (js/require "react-native-desktop-shortcuts"))
(def react-native-firebase (fn [] #js {}))
+(def touchid (fn [] #js {}))
(def camera (fn [] #js {:default #js {:constants {:Aspect "Portrait"}}}))
(def status-keycard (fn [] #js {:default #js {}}))
(def dialogs (fn [] #js {}))
diff --git a/react-native/src/mobile/status_im/react_native/js_dependencies.cljs b/react-native/src/mobile/status_im/react_native/js_dependencies.cljs
index c9f14f99c4..9c71e4c655 100644
--- a/react-native/src/mobile/status_im/react_native/js_dependencies.cljs
+++ b/react-native/src/mobile/status_im/react_native/js_dependencies.cljs
@@ -10,6 +10,7 @@
(def status-keycard (js-require/js-require "react-native-status-keycard"))
(def realm (js/require "realm"))
(def webview-bridge (js-require/js-require "react-native-webview-bridge"))
+(defn touchid [] (.-default (js/require "react-native-touch-id")))
(def webview (js-require/js-require "react-native-webview"))
(def securerandom (js-require/js-require "react-native-securerandom"))
(defn secure-random [] (.-generateSecureRandom (securerandom)))
diff --git a/src/status_im/accounts/core.cljs b/src/status_im/accounts/core.cljs
index c38bf7583d..6bf57e65d0 100644
--- a/src/status_im/accounts/core.cljs
+++ b/src/status_im/accounts/core.cljs
@@ -80,6 +80,15 @@
(assoc settings :chaos-mode? chaos-mode?)
{})))))
+(fx/defn switch-biometric-auth
+ {:events [:accounts.ui/switch-biometric-auth]}
+ [{:keys [db] :as cofx} biometric-auth?]
+ (when (:account/account db)
+ (let [settings (get-in db [:account/account :settings])]
+ (accounts.update/update-settings cofx
+ (assoc settings :biometric-auth? biometric-auth?)
+ {}))))
+
(fx/defn enable-notifications [cofx desktop-notifications?]
(accounts.update/account-update cofx
{:desktop-notifications? desktop-notifications?}
diff --git a/src/status_im/accounts/login/core.cljs b/src/status_im/accounts/login/core.cljs
index 5d61bcefc9..9b03fdb26f 100644
--- a/src/status_im/accounts/login/core.cljs
+++ b/src/status_im/accounts/login/core.cljs
@@ -19,6 +19,7 @@
[status-im.utils.keychain.core :as keychain]
[status-im.utils.platform :as platform]
[status-im.utils.security :as security]
+ [status-im.biometric-auth.core :as biometric-auth]
[status-im.utils.types :as types]
[status-im.utils.universal-links.core :as universal-links]
[status-im.wallet.core :as wallet]
@@ -297,7 +298,7 @@
[_ address]
{:keychain/can-save-user-password? nil
:keychain/get-user-password [address
- #(re-frame/dispatch [:accounts.login.callback/get-user-password-success %])]})
+ #(re-frame/dispatch [:accounts.login.callback/get-user-password-success % address])]})
(fx/defn open-login [{:keys [db] :as cofx} address photo-path name]
(let [keycard-account? (get-in db [:accounts/accounts address :keycard-instance-uid])]
@@ -315,13 +316,26 @@
(get-user-password address)))))
(fx/defn open-login-callback
+ {:events [:accounts.login.callback/biometric-auth-done]}
+ [{:keys [db] :as cofx} password biometric-auth-result]
+ (let [{:keys [bioauth-success bioauth-notrequired bioauth-message]} biometric-auth-result]
+ (if (and password
+ (or bioauth-success bioauth-notrequired))
+ (fx/merge cofx
+ {:db (assoc-in db [:accounts/login :password] password)}
+ (navigation/navigate-to-cofx :progress nil)
+ (user-login false))
+ (fx/merge cofx
+ (when bioauth-message
+ {:utils/show-popup {:title (i18n/label :t/biometric-auth-login-error-title) :content bioauth-message}})
+ (navigation/navigate-to-cofx :login nil)))))
+
+(fx/defn do-biometric-auth
[{:keys [db] :as cofx} password]
- (if password
- (fx/merge cofx
- {:db (assoc-in db [:accounts/login :password] password)}
- (navigation/navigate-to-cofx :progress nil)
- (user-login false))
- (navigation/navigate-to-cofx cofx :login nil)))
+ (biometric-auth/authenticate-fx cofx
+ #(re-frame/dispatch [:accounts.login.callback/biometric-auth-done password %])
+ {:reason (i18n/label :t/biometric-auth-reason-login)
+ :ios-fallback-label (i18n/label :t/biometric-auth-login-ios-fallback-label)}))
(re-frame/reg-fx
:accounts.login/login
diff --git a/src/status_im/biometric_auth/core.cljs b/src/status_im/biometric_auth/core.cljs
new file mode 100644
index 0000000000..bb5a4f6635
--- /dev/null
+++ b/src/status_im/biometric_auth/core.cljs
@@ -0,0 +1,93 @@
+(ns status-im.biometric-auth.core
+ (:require [re-frame.core :as re-frame]
+ [status-im.utils.platform :as platform]
+ [status-im.i18n :as i18n]
+ [status-im.utils.fx :as fx]
+ [status-im.ui.components.colors :as colors]
+ [status-im.native-module.core :as status]
+ [status-im.react-native.js-dependencies :as rn]))
+
+;; currently, for android, react-native-touch-id
+;; is not returning supported biometric type
+;; defaulting to :fingerprint
+(def android-default-support :fingerprint)
+
+;;; android blacklist based on device info:
+
+(def deviceinfo (status/get-device-model-info))
+
+;; {:model ?
+;; :brand "Xiaomi"
+;; :build-id "13D15"
+;; :device-id "goldfish"
+;; more info on https://github.com/react-native-community/react-native-device-info
+
+(def android-device-blacklisted?
+ (cond
+ (= (:brand deviceinfo) "bannedbrand") true
+ :else false))
+
+;; biometric auth config
+;; https://github.com/naoufal/react-native-touch-id#authenticatereason-config
+(defn- authenticate-options [ios-fallback-label]
+ (clj->js (merge
+ {:unifiedErrors true}
+ (when platform/ios?
+ {:passcodeFallback false
+ :fallbackLabel (or ios-fallback-label "")})
+ (when platform/android?
+ {:title (i18n/label :t/biometric-auth-android-title)
+ :imageColor colors/blue
+ :imageErrorColor colors/red
+ :sensorDescription (i18n/label :t/biometric-auth-android-sensor-desc)
+ :sensorErrorDescription (i18n/label :t/biometric-auth-android-sensor-error-desc)
+ :cancelText (i18n/label :cancel)}))))
+
+(defn- get-error-message
+ "must return an error message for the user"
+ [touchid-error-code]
+ (cond
+ ;; no message if user canceled or falled back to password
+ (= touchid-error-code "USER_CANCELED") nil
+ (= touchid-error-code "USER_FALLBACK") nil
+ ;; add here more specific errors if needed
+ ;; https://github.com/naoufal/react-native-touch-id#unified-errors
+ :else (i18n/label :t/biometric-auth-error {:code touchid-error-code})))
+
+(def success-result
+ {:bioauth-success true})
+
+(defn- generate-error-result [touchid-error-obj]
+ (let [code (aget touchid-error-obj "code")]
+ {:bioauth-success false
+ :bioauth-code code
+ :bioauth-message (get-error-message code)}))
+
+(defn- do-get-supported [callback]
+ (-> (.isSupported (rn/touchid))
+ (.then #(callback (or (keyword %) android-default-support)))
+ (.catch #(callback nil))))
+
+(defn get-supported [callback]
+ (cond platform/ios? (do-get-supported callback)
+ platform/android? (if android-device-blacklisted?
+ (callback false)
+ (do-get-supported callback))
+ :else (callback false)))
+
+(defn authenticate
+ ([cb]
+ (authenticate cb nil))
+ ([cb {:keys [reason ios-fallback-label]}]
+ (-> (.authenticate (rn/touchid) reason (authenticate-options ios-fallback-label))
+ (.then #(cb success-result))
+ (.catch #(cb (generate-error-result %))))))
+
+(fx/defn authenticate-fx
+ [_ cb options]
+ {:biometric-auth/authenticate [cb options]})
+
+(re-frame/reg-fx
+ :biometric-auth/authenticate
+ (fn [[cb options]]
+ (authenticate #(cb %) options)))
\ No newline at end of file
diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs
index 471bfbd4a8..019c46bdc2 100644
--- a/src/status_im/constants.cljs
+++ b/src/status_im/constants.cljs
@@ -5,6 +5,8 @@
(def ethereum-rpc-url "http://localhost:8545")
+(def ms-in-bg-for-require-bioauth 5000)
+
(def content-type-text "text/plain")
(def content-type-sticker "sticker")
(def content-type-status "status")
diff --git a/src/status_im/events.cljs b/src/status_im/events.cljs
index bbf0c67e76..1a3fa97832 100644
--- a/src/status_im/events.cljs
+++ b/src/status_im/events.cljs
@@ -60,6 +60,7 @@
[status-im.wallet.core :as wallet]
[status-im.wallet.db :as wallet.db]
[status-im.web3.core :as web3]
+ [status-im.biometric-auth.core :as biomentric-auth]
[taoensso.timbre :as log]
[status-im.wallet.custom-tokens.core :as custom-tokens]))
@@ -202,6 +203,20 @@
:on-accept open-chaos-unicorn-day-link}})
(accounts/switch-chaos-mode chaos-mode?)))))
+(handlers/register-handler-fx
+ :accounts.ui/biometric-auth-switched
+ (fn [cofx [_ biometric-auth?]]
+ (if biometric-auth?
+ (biomentric-auth/authenticate-fx
+ cofx
+ (fn [{:keys [bioauth-success bioauth-message]}]
+ (when bioauth-success
+ (re-frame/dispatch [:accounts.ui/switch-biometric-auth true]))
+ (when bioauth-message
+ (utils/show-popup (i18n/label :t/biometric-auth-reason-verify) bioauth-message)))
+ {:reason (i18n/label :t/biometric-auth-reason-verify)})
+ (accounts/switch-biometric-auth cofx false))))
+
(handlers/register-handler-fx
:accounts.ui/notifications-enabled
(fn [cofx [_ desktop-notifications?]]
@@ -325,8 +340,11 @@
(handlers/register-handler-fx
:accounts.login.callback/get-user-password-success
- (fn [cofx [_ password]]
- (accounts.login/open-login-callback cofx password)))
+ (fn [{:keys [db] :as cofx} [_ password address]]
+ (let [biometric-auth? (get-in db [:accounts/accounts address :settings :biometric-auth?])]
+ (if (and password biometric-auth?)
+ (accounts.login/do-biometric-auth cofx password)
+ (accounts.login/open-login-callback cofx password {:bioauth-notrequired true})))))
;; accounts logout module
diff --git a/src/status_im/init/core.cljs b/src/status_im/init/core.cljs
index 9d381809cd..9a0a4a0fa7 100644
--- a/src/status_im/init/core.cljs
+++ b/src/status_im/init/core.cljs
@@ -22,6 +22,7 @@
[status-im.utils.fx :as fx]
[status-im.utils.keychain.core :as keychain]
[status-im.utils.platform :as platform]
+ [status-im.biometric-auth.core :as biometric-auth]
[taoensso.timbre :as log]))
(defn init-store!
@@ -66,6 +67,7 @@
(fx/defn start-app [cofx]
(fx/merge cofx
{:init/get-device-UUID nil
+ :init/get-supported-biometric-auth nil
:init/restore-native-settings nil
:ui/listen-to-window-dimensions-change nil
:notifications/init nil
@@ -79,7 +81,7 @@
[{{:keys [view-id hardwallet
initial-props desktop/desktop
network-status network peers-count peers-summary device-UUID
- push-notifications/stored network/type]
+ supported-biometric-auth push-notifications/stored network/type]
:node/keys [status]
:or {network (get app-db :network)}} :db}]
{:db (assoc app-db
@@ -94,6 +96,7 @@
:network/type type
:hardwallet hardwallet
:device-UUID device-UUID
+ :supported-biometric-auth supported-biometric-auth
:view-id view-id
:push-notifications/stored stored)})
@@ -109,6 +112,11 @@
[{:keys [db]} device-uuid]
{:db (assoc db :device-UUID device-uuid)})
+(fx/defn set-supported-biometric-auth
+ {:events [:init.callback/get-supported-biometric-auth-success]}
+ [{:keys [db]} supported-biometric-auth]
+ {:db (assoc db :supported-biometric-auth supported-biometric-auth)})
+
(fx/defn handle-init-store-error
[encryption-key cofx]
{:ui/show-confirmation
@@ -159,7 +167,7 @@
:keys [accounts/accounts accounts/create networks/networks network
network-status peers-count peers-summary view-id navigation-stack
mailserver/mailservers
- desktop/desktop hardwallet custom-fleets
+ desktop/desktop hardwallet custom-fleets supported-biometric-auth
device-UUID semaphores accounts/login]
:node/keys [status on-ready]
:or {network (get app-db :network)}} db
@@ -187,6 +195,7 @@
:peers-summary peers-summary
:peers-count peers-count
:device-UUID device-UUID
+ :supported-biometric-auth supported-biometric-auth
:semaphores semaphores
:hardwallet hardwallet
:web3 web3)
@@ -254,6 +263,11 @@
(fn []
(status/get-device-UUID #(re-frame/dispatch [:init.callback/get-device-UUID-success %]))))
+(re-frame/reg-fx
+ :init/get-supported-biometric-auth
+ (fn []
+ (biometric-auth/get-supported #(re-frame/dispatch [:init.callback/get-supported-biometric-auth-success %]))))
+
(re-frame/reg-fx
:init/reset-data
reset-data!)
diff --git a/src/status_im/native_module/core.cljs b/src/status_im/native_module/core.cljs
index 81e76c3aad..0f09fa09f1 100644
--- a/src/status_im/native_module/core.cljs
+++ b/src/status_im/native_module/core.cljs
@@ -94,6 +94,9 @@
(defn is24Hour []
(native-module/is24Hour))
+(defn get-device-model-info []
+ (native-module/get-device-model-info))
+
(def extract-group-membership-signatures native-module/extract-group-membership-signatures)
(def sign-group-membership native-module/sign-group-membership)
diff --git a/src/status_im/native_module/impl/module.cljs b/src/status_im/native_module/impl/module.cljs
index cf73522fc4..5d505949d1 100644
--- a/src/status_im/native_module/impl/module.cljs
+++ b/src/status_im/native_module/impl/module.cljs
@@ -156,6 +156,13 @@
(when (status)
(.-is24Hour (status))))
+(defn get-device-model-info []
+ (when status
+ {:model (.-model status)
+ :brand (.-brand status)
+ :build-id (.-buildId status)
+ :device-id (.-deviceId status)}))
+
(defn update-mailservers [enodes on-result]
(when (status)
(.updateMailservers (status) enodes on-result)))
diff --git a/src/status_im/subs.cljs b/src/status_im/subs.cljs
index 63846087ba..14b0993c82 100644
--- a/src/status_im/subs.cljs
+++ b/src/status_im/subs.cljs
@@ -89,6 +89,7 @@
(reg-root-key-sub :get-pairing-installations :pairing/installations)
(reg-root-key-sub :network/type :network/type)
(reg-root-key-sub :tooltips :tooltips)
+(reg-root-key-sub :supported-biometric-auth :supported-biometric-auth)
;;profile
(reg-root-key-sub :my-profile/seed :my-profile/seed)
diff --git a/src/status_im/ui/screens/db.cljs b/src/status_im/ui/screens/db.cljs
index c2c0041d5b..be1d02cd5c 100644
--- a/src/status_im/ui/screens/db.cljs
+++ b/src/status_im/ui/screens/db.cljs
@@ -93,6 +93,7 @@
(spec/def ::network-status (spec/nilable keyword?))
(spec/def ::app-state string?)
+(spec/def ::app-in-background-since (spec/nilable number?))
;;;;NODE
@@ -176,6 +177,10 @@
(spec/def ::device-UUID (spec/nilable string?))
+;;;; Supported Biometric authentication types
+
+(spec/def ::supported-biometric-auth (spec/nilable #{:FaceID :TouchID :fingerprint}))
+
;;;;UNIVERSAL LINKS
(spec/def :universal-links/url (spec/nilable string?))
@@ -295,6 +300,7 @@
::network
::chain
::app-state
+ ::app-in-background-since
::semaphores
::hardwallet
:navigation/view-id
@@ -332,6 +338,7 @@
:prices/prices-loading?
:notifications/notifications
::device-UUID
+ ::supported-biometric-auth
::collectible
::collectibles
::extensions-store
diff --git a/src/status_im/ui/screens/events.cljs b/src/status_im/ui/screens/events.cljs
index 4dcc391c3e..4239d3190d 100644
--- a/src/status_im/ui/screens/events.cljs
+++ b/src/status_im/ui/screens/events.cljs
@@ -32,7 +32,10 @@
[status-im.utils.handlers :as handlers]
[status-im.utils.http :as http]
[status-im.utils.utils :as utils]
- [status-im.wallet.core :as wallet]))
+ [status-im.wallet.core :as wallet]
+ [status-im.i18n :as i18n]
+ [status-im.biometric-auth.core :as biometric-auth]
+ [status-im.constants :as const]))
(defn- http-get [{:keys [url response-validator success-event-creator failure-event-creator timeout-ms]}]
(let [on-success #(re-frame/dispatch (success-event-creator %))
@@ -136,18 +139,51 @@
(fn [{:keys [db]} [_ path v]]
{:db (assoc-in db path v)}))
-(fx/defn on-return-from-background [cofx]
+(def authentication-options
+ {:reason (i18n/label :t/biometric-auth-reason-login)})
+
+(defn- on-biometric-auth-result [{:keys [bioauth-success bioauth-code bioauth-message]}]
+ (when-not bioauth-success
+ (if (= bioauth-code "USER_FALLBACK")
+ (re-frame/dispatch [:accounts.logout.ui/logout-confirmed])
+ (utils/show-confirmation {:title (i18n/label :t/biometric-auth-confirm-title)
+ :content (or bioauth-message (i18n/label :t/biometric-auth-confirm-message))
+ :confirm-button-text (i18n/label :t/biometric-auth-confirm-try-again)
+ :cancel-button-text (i18n/label :t/biometric-auth-confirm-logout)
+ :on-accept #(biometric-auth/authenticate on-biometric-auth-result authentication-options)
+ :on-cancel #(re-frame/dispatch [:accounts.logout.ui/logout-confirmed])}))))
+
+(fx/defn on-return-from-background [{:keys [db now] :as cofx}]
+ (let [app-in-background-since (get db :app-in-background-since)
+ signed-up? (get-in db [:account/account :signed-up?])
+ biometric-auth? (get-in db [:account/account :settings :biometric-auth?])
+ requires-bio-auth (and
+ signed-up?
+ biometric-auth?
+ (some? app-in-background-since)
+ (>= (- now app-in-background-since)
+ const/ms-in-bg-for-require-bioauth))]
+ (fx/merge cofx
+ {:db (assoc db :app-in-background-since nil)}
+ (mailserver/process-next-messages-request)
+ (hardwallet/return-back-from-nfc-settings)
+ #(when requires-bio-auth
+ (biometric-auth/authenticate-fx % on-biometric-auth-result authentication-options)))))
+
+(fx/defn on-going-in-background [{:keys [db now] :as cofx}]
(fx/merge cofx
- (mailserver/process-next-messages-request)
- (hardwallet/return-back-from-nfc-settings)))
+ {:db (assoc db :app-in-background-since now)}))
(defn app-state-change [state {:keys [db] :as cofx}]
- (let [app-coming-from-background? (= state "active")]
+ (let [app-coming-from-background? (= state "active")
+ app-going-in-background? (= state "background")]
(fx/merge cofx
{::app-state-change-fx state
:db (assoc db :app-state state)}
#(when app-coming-from-background?
- (on-return-from-background %)))))
+ (on-return-from-background %))
+ #(when app-going-in-background?
+ (on-going-in-background %)))))
(handlers/register-handler-fx
:app-state-change
diff --git a/src/status_im/ui/screens/profile/user/views.cljs b/src/status_im/ui/screens/profile/user/views.cljs
index 4b9c07d132..d020aff964 100644
--- a/src/status_im/ui/screens/profile/user/views.cljs
+++ b/src/status_im/ui/screens/profile/user/views.cljs
@@ -23,6 +23,7 @@
[status-im.utils.identicon :as identicon]
[status-im.utils.platform :as platform]
[status-im.utils.universal-links.core :as universal-links]
+ [status-im.biometric-auth.core :as biometric-auth]
[status-im.utils.utils :as utils])
(:require-macros [status-im.utils.views :refer [defview letsubs]]))
@@ -180,7 +181,7 @@
:active? logged-in?
:action-fn #(re-frame/dispatch [:accounts.logout.ui/logout-pressed])}]]]]))
-(defview advanced-settings [{:keys [network networks dev-mode? settings]} on-show]
+(defview advanced-settings [{:keys [network networks dev-mode? settings]} on-show supported-biometric-auth]
{:component-did-mount on-show}
[react/view
(when (and config/extensions-enabled? dev-mode?)
@@ -239,10 +240,18 @@
[profile.components/settings-switch-item
{:label-kw :t/chaos-mode
:value (:chaos-mode? settings)
- :action-fn #(re-frame/dispatch [:accounts.ui/chaos-mode-switched %])}]])
+ :action-fn #(re-frame/dispatch [:accounts.ui/chaos-mode-switched %])}]
+ (when dev-mode?
+ [profile.components/settings-item-separator]
+ [profile.components/settings-switch-item
+ {:label-kw :t/biometric-auth-setting-label
+ :value (:biometric-auth? settings)
+ :active? (some? supported-biometric-auth)
+ :action-fn #(re-frame/dispatch [:accounts.ui/biometric-auth-switched %])}])])
(defview advanced [params on-show]
- (letsubs [advanced? [:my-profile/advanced?]]
+ (letsubs [advanced? [:my-profile/advanced?]
+ supported-biometric-auth [:supported-biometric-auth]]
{:component-will-unmount #(re-frame/dispatch [:set :my-profile/advanced? false])}
[react/view {:padding-bottom 16}
[react/touchable-highlight {:on-press #(re-frame/dispatch [:set :my-profile/advanced? (not advanced?)])
@@ -254,7 +263,7 @@
(i18n/label :t/wallet-advanced)]
[icons/icon (if advanced? :main-icons/dropdown-up :main-icons/dropdown) {:color colors/blue}]]]]]
(when advanced?
- [advanced-settings params on-show])]))
+ [advanced-settings params on-show supported-biometric-auth])]))
(defn share-profile-item
[{:keys [public-key photo-path] :as current-account}]
diff --git a/src/status_im/utils/keychain/core.cljs b/src/status_im/utils/keychain/core.cljs
index 693ea853e7..3607f9c233 100644
--- a/src/status_im/utils/keychain/core.cljs
+++ b/src/status_im/utils/keychain/core.cljs
@@ -4,6 +4,7 @@
[status-im.react-native.js-dependencies :as rn]
[status-im.utils.platform :as platform]
[status-im.utils.security :as security]
+ [status-im.biometric-auth.core :as biometric-auth]
[status-im.native-module.core :as status]))
(def key-bytes 64)
@@ -84,6 +85,10 @@
(enum-val "ACCESS_CONTROL" "BIOMETRY_ANY_OR_DEVICE_PASSCODE")}))
(.then callback)))
+;; Android and iOS
+(defn- biometric-auth-available? [callback]
+ (biometric-auth/get-supported #(callback (some? %))))
+
;; Stores the password for the address to the Keychain
(defn save-user-password [address password callback]
(-> (.setInternetCredentials (rn/keychain) address address password keychain-secure-hardware (clj->js keychain-restricted-availability))
@@ -112,11 +117,14 @@
;; Resolves to `false` if the device doesn't have neither a passcode nor a biometry auth.
(defn can-save-user-password? [callback]
(cond
- platform/ios? (device-encrypted? callback)
+ platform/ios? (check-conditions callback
+ device-encrypted?
+ biometric-auth-available?)
platform/android? (check-conditions
callback
secure-hardware-available?
+ biometric-auth-available?
device-not-rooted?)
:else (callback false)))
diff --git a/test/cljs/status_im/react_native/js_dependencies.cljs b/test/cljs/status_im/react_native/js_dependencies.cljs
index 3afee1d369..99d5e57eec 100644
--- a/test/cljs/status_im/react_native/js_dependencies.cljs
+++ b/test/cljs/status_im/react_native/js_dependencies.cljs
@@ -25,6 +25,7 @@
(def vector-icons (fn [] #js {:default #js {}}))
(def webview-bridge (fn [] #js {:default #js {}}))
(def webview (fn [] #js {:WebView #js {}}))
+(def touchid (fn [] #js {}))
(def svg (fn [] #js {:default #js {}}))
(def status-keycard (fn [] #js {:default #js {}}))
diff --git a/translations/en.json b/translations/en.json
index 00f136ff70..2ffed80191 100644
--- a/translations/en.json
+++ b/translations/en.json
@@ -1029,6 +1029,19 @@
"load-messages-before": "before {{date}}",
"open-dapp-store": "Discover ÐApps",
"browsed-websites": "Browsed websites will appear here.",
+ "biometric-auth-error": "Unable perform biometric authentication ({{code}})",
+ "biometric-auth-reason-verify": "Verify authentication",
+ "biometric-auth-reason-login": "Login in Status",
+ "biometric-auth-confirm-try-again": "Try again",
+ "biometric-auth-confirm-logout": "Relogin",
+ "biometric-auth-confirm-message": "Biometric authentication is required to continue, if not possible please relogin using account password",
+ "biometric-auth-confirm-title": "You must authenticate!",
+ "biometric-auth-login-error-title": "Biometric authentication error",
+ "biometric-auth-login-ios-fallback-label": "Enter Password",
+ "biometric-auth-setting-label": "Use biometric authentication",
+ "biometric-auth-android-title": "Authentication Required",
+ "biometric-auth-android-sensor-desc": "Touch sensor",
+ "biometric-auth-android-sensor-error-desc": "Failed",
"dapps-can-access" : "ÐApps can access my wallet and contact code",
"require-my-permission" : "Require my permission",
"might-break" : "Might break some ÐApps",