Implement "Save Password" for iOS.

Update react-native-keychain to 3.0.0 release.

Signed-off-by: Igor Mandrigin <i@mandrigin.ru>
This commit is contained in:
Igor Mandrigin 2018-08-23 16:27:52 +02:00
parent cf0a49b3b1
commit 02545cc3d0
No known key found for this signature in database
GPG Key ID: 4A0EDDE26E66BC8B
15 changed files with 345 additions and 143 deletions

View File

@ -38,7 +38,7 @@ PODS:
- React
- React/Core (0.55.4):
- yoga (= 0.55.4.React)
- RNKeychain (3.0.0-rc.3):
- RNKeychain (3.0.0):
- React
- yoga (0.55.4.React)
@ -83,6 +83,6 @@ SPEC CHECKSUMS:
RNKeychain: 627c6095cef215dd3d9804a9a9cf45ab96aa3997
yoga: a23273df0088bf7f2bb7e5d7b00044ea57a2a54a
PODFILE CHECKSUM: 7d2c351f8b90d3ad2b11fdcfde727875f4c2eebf
PODFILE CHECKSUM: e5ecd57d8448253eb06b209c4e1c5230a2634a1d
COCOAPODS: 1.4.0

View File

@ -6739,9 +6739,9 @@
}
},
"react-native-keychain": {
"version": "3.0.0-rc.3",
"resolved": "https://registry.npmjs.org/react-native-keychain/-/react-native-keychain-3.0.0-rc.3.tgz",
"integrity": "sha512-ijWfHmxTPKnrHtPJiDbKW3D6lRH8O9wbCNEE3xlxEg1WZT+VhP6iiF+HUansNYuxL7Hh7k41GSFfvr3xumfmXA=="
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/react-native-keychain/-/react-native-keychain-3.0.0.tgz",
"integrity": "sha512-0incABt1+aXsZvG34mDV57KKanSB+iMHmxvWv+N6lgpNLaSoqrCxazjbZdeqD4qJ7Z+Etp5CLf/4v1aI+sNLBw=="
},
"react-native-level-fs": {
"version": "3.0.0",

View File

@ -46,7 +46,7 @@
"react-native-image-crop-picker": "0.18.1",
"react-native-image-resizer": "1.0.0",
"react-native-invertible-scroll-view": "1.1.0",
"react-native-keychain": "3.0.0-rc.3",
"react-native-keychain": "^3.0.0",
"react-native-level-fs": "3.0.0",
"react-native-os": "1.1.0",
"react-native-qrcode": "0.2.6",

View File

@ -174,7 +174,7 @@
:transaction-moved-title :photos-access-error :hash
:removed-from-chat :done :remove-from-contacts :delete-chat :new-group-chat
:edit-chats :wallet :wallet-exchange :wallet-request :sign-in
:datetime-yesterday :create-new-account :sign-in-to-status :dapp-profile
:datetime-yesterday :create-new-account :sign-in-to-status :save-password :save-password-unavailable :dapp-profile
:sign-later-text :datetime-ago :no-hashtags-discovered-body :contacts
:search-chat :got-it :delete-group-confirmation :public-chats
:not-applicable :move-to-internal-failure-message :active-online

View File

@ -385,6 +385,8 @@
:sign-in-to-status "Sign in to Status"
:sign-in "Sign in"
:sign-in-to-another "Sign in to another account"
:save-password "Save password until logout"
:save-password-unavailable "Set device passcode to save password"
:wrong-password "Wrong password"
:enter-password "Enter password"

View File

@ -4,7 +4,7 @@
[status-im.ui.components.colors :as colors]
[status-im.ui.components.tooltip.views :as tooltip]))
(defn text-input-with-label [{:keys [label content error style height container] :as props}]
(defn text-input-with-label [{:keys [label content error style height container text] :as props}]
[react/view
(when label
[react/text {:style styles/label}
@ -15,6 +15,7 @@
{:style (merge styles/input style)
:placeholder-text-color colors/gray
:auto-focus true
:value text
:auto-capitalize :none}
(dissoc props :style :height))]
(when content content)]

View File

@ -2,6 +2,7 @@
(:require status-im.ui.screens.accounts.login.navigation
[re-frame.core :as re-frame]
[status-im.utils.handlers :as handlers]
[status-im.utils.handlers-macro :as handlers-macro]
[status-im.ui.screens.accounts.login.models :as models]))
;;;; FX
@ -10,8 +11,8 @@
(re-frame/reg-fx
:login
(fn [[address password]]
(models/login! address password)))
(fn [[address password save-password]]
(models/login! address password save-password)))
(re-frame/reg-fx
:clear-web-data
@ -29,27 +30,36 @@
(fn [cofx [_ address photo-path name]]
(models/open-login address photo-path name cofx)))
(handlers/register-handler-fx
:do-login
(fn [cofx [_ address photo-path name password]]
(handlers-macro/merge-fx cofx
(models/navigate-to-login address photo-path name password)
;; models/login-account takes care about empty password
(models/login-account address password (not (empty? password))))))
(handlers/register-handler-fx
:login-account-internal
(fn [cofx [_ address password]]
(models/login-account-internal address password cofx)))
(fn [cofx [_ address password save-password]]
(models/login-account-internal address password save-password cofx)))
(handlers/register-handler-fx
:start-node
(fn [cofx [_ address password]]
(models/start-node address password cofx)))
(fn [cofx [_ address password save-password]]
(models/start-node address password save-password cofx)))
(handlers/register-handler-fx
:login-account
(fn [cofx [_ address password]]
(models/login-account address password cofx)))
(fn [cofx [_ address password save-password]]
(models/login-account address password save-password cofx)))
(handlers/register-handler-fx
:login-handler
(fn [cofx [_ login-result address]]
(models/login-handler login-result address cofx)))
(fn [cofx [_ login-result address password save-password]]
(models/login-handler login-result address password save-password cofx)))
(handlers/register-handler-fx
:change-account-handler
(fn [cofx [_ address]]
(models/change-account-handler address cofx)))

View File

@ -7,6 +7,7 @@
[status-im.utils.config :as config]
[status-im.utils.keychain.core :as keychain]
[status-im.utils.notifications :as notifications]
[taoensso.timbre :as log]
[status-im.utils.platform :as platform]
[status-im.utils.universal-links.core :as universal-links]))
@ -14,8 +15,10 @@
(defn stop-node! [] (status/stop-node))
(defn login! [address password]
(status/login address password #(re-frame/dispatch [:login-handler % address])))
(defn login! [address password save-password]
(status/login address
password
#(re-frame/dispatch [:login-handler % address password save-password])))
(defn clear-web-data! []
(status/clear-web-data))
@ -34,22 +37,27 @@
;;;; Handlers
(defn open-login [address photo-path name {db :db}]
(defn navigate-to-login [address photo-path name password {db :db}]
{:db (update db
:accounts/login assoc
:address address
:photo-path photo-path
:password password
:name name)
:can-save-user-password [#(re-frame/dispatch [:set-in [:accounts/login :can-save-password] %])]
:dispatch [:navigate-to :login]})
(defn wrap-with-login-account-fx [db address password]
(defn wrap-with-login-account-fx [db address password save-password]
{:db db
:login [address password]})
:login [address password save-password]})
(defn login-account-internal [address password {db :db}]
(defn open-login [address photo-path name cofx]
{:get-user-password [address #(re-frame/dispatch [:do-login address photo-path name %])]})
(defn login-account-internal [address password save-password {db :db}]
(wrap-with-login-account-fx
(assoc db :node/after-start nil)
address password))
address password save-password))
(defn- add-custom-bootnodes [config network all-bootnodes]
(let [bootnodes (as-> all-bootnodes $
@ -77,17 +85,17 @@
:network network
:config config}))
(defn- wrap-with-initialize-geth-fx [db address password]
(defn- wrap-with-initialize-geth-fx [db address password save-password]
(let [{:keys [network config]} (get-network-by-address db address)]
{:initialize-geth-fx config
:db (assoc db
:network network
:node/after-start [:login-account-internal address password])}))
:node/after-start [:login-account-internal address password save-password])}))
(defn start-node [address password {db :db}]
(defn start-node [address password save-password {db :db}]
(wrap-with-initialize-geth-fx
(assoc db :node/after-stop nil)
address password))
address password save-password))
(defn- wrap-with-stop-node-fx [db address password]
{:db (assoc db :node/after-stop [:start-node address password])
@ -98,7 +106,7 @@
(and config/bootnodes-settings-enabled?
use-custom-bootnodes)))
(defn login-account [address password {{:keys [network status-node-started?] :as db} :db}]
(defn login-account [address password save-password {{:keys [network status-node-started?] :as db} :db}]
(let [{use-custom-bootnodes :use-custom-bootnodes
account-network :network} (get-network-by-address db address)
db' (-> db
@ -113,17 +121,21 @@
:else
wrap-with-stop-node-fx)]
(wrap-fn db' address password)))
(when-not (empty? password)
(wrap-fn db' address password save-password))))
(defn login-handler [login-result address {db :db}]
(defn login-handler [login-result address password save-password {db :db}]
(let [data (types/json->clj login-result)
error (:error data)
success (zero? (count error))
db' (assoc-in db [:accounts/login :processing] false)]
(if success
{:db db
:clear-web-data nil
:change-account [address]}
(merge
{:db db
:clear-web-data nil
:change-account [address]}
(when save-password
{:save-user-password [address password]}))
{:db (assoc-in db' [:accounts/login :error] error)})))
(defn change-account-handler [address {{:keys [view-id] :as db} :db :as cofx}]

View File

@ -3,4 +3,4 @@
(defmethod nav/preload-data! :login
[db]
(update db :accounts/login dissoc :error :password))
(update db :accounts/login dissoc :error))

View File

@ -38,3 +38,15 @@
(def password-container
{:margin-top 24})
(def save-password-checkbox-container
{:margin-top 0
:flex-direction :row
:align-items :center})
(def save-password-unavailable
{:margin-top 8
:width "100%"
:text-align :center
:flex-direction :row
:align-items :center})

View File

@ -2,6 +2,8 @@
(:require-macros [status-im.utils.views :refer [defview letsubs]])
(:require [clojure.string :as string]
[status-im.ui.screens.accounts.styles :as ast]
[status-im.ui.screens.profile.components.views :as profile.components]
[status-im.ui.components.checkbox.view :as checkbox]
[status-im.ui.components.text-input.view :as text-input]
[status-im.ui.components.status-bar.view :as status-bar]
[status-im.ui.components.toolbar.view :as toolbar]
@ -14,6 +16,7 @@
[status-im.chat.views.photos :as photos]
[re-frame.core :as re-frame]
[cljs.spec.alpha :as spec]
[status-im.utils.platform :as platform]
[status-im.ui.screens.accounts.db :as db]))
(defn login-toolbar [can-navigate-back?]
@ -23,9 +26,9 @@
[toolbar/nav-button act/default-back])
[toolbar/content-title (i18n/label :t/sign-in-to-status)]])
(defn login-account [password-text-input address password]
(defn login-account [password-text-input address password save-password]
(.blur password-text-input)
(re-frame/dispatch [:login-account address password]))
(re-frame/dispatch [:login-account address password save-password]))
(defn- error-key [error]
;; TODO Improve selection logic when status-go provide an error code
@ -49,29 +52,49 @@
name]]])
(defview login []
(letsubs [{:keys [address photo-path name password error processing]} [:get :accounts/login]
(letsubs [{:keys [address photo-path name password error processing save-password can-save-password]} [:get :accounts/login]
can-navigate-back? [:can-navigate-back?]
password-text-input (atom nil)]
;; due to async nature of UI, this function can be called when
;; :accounts/login is already removed
;; (e.g. when the data is removed but we didn't navigate to the next screen)
;; in that case, we will show a basic progress bar.
[react/keyboard-avoiding-view {:style ast/accounts-view}
[status-bar/status-bar]
[login-toolbar can-navigate-back?]
[components.common/separator]
[react/view styles/login-view
[react/view styles/login-badge-container
[account-login-badge photo-path name]
[react/view {:style styles/password-container
:important-for-accessibility :no-hide-descendants}
[text-input/text-input-with-label
{:label (i18n/label :t/password)
:placeholder (i18n/label :t/password)
:ref #(reset! password-text-input %)
:auto-focus can-navigate-back? ;;this needed because keyboard overlays testfairy alert
:on-submit-editing #(login-account @password-text-input address password)
:on-change-text #(do
(re-frame/dispatch [:set-in [:accounts/login :password] %])
(re-frame/dispatch [:set-in [:accounts/login :error] ""]))
:secure-text-entry true
:error (when (pos? (count error)) (i18n/label (error-key error)))}]]]]
;; so, if this component is rendered with no data
(if (or (empty? address) (empty? photo-path))
;; we will show an activiy indicator
[react/view styles/processing-view
[components/activity-indicator {:animating true}]]
;; otherwise, we will render it properly
[react/view styles/login-badge-container
[account-login-badge photo-path name]
[react/view {:style styles/password-container
:important-for-accessibility :no-hide-descendants}
[text-input/text-input-with-label
{:label (i18n/label :t/password)
:placeholder (i18n/label :t/password)
:ref #(reset! password-text-input %)
:auto-focus can-navigate-back? ;;this needed because keyboard overlays testfairy alert
:on-submit-editing #(login-account @password-text-input address password save-password)
:on-change-text #(do
(re-frame/dispatch [:set-in [:accounts/login :password] %])
(re-frame/dispatch [:set-in [:accounts/login :error] ""]))
:secure-text-entry true
:text password
:error (when (pos? (count error)) (i18n/label (error-key error)))}]]
(when platform/ios?
[react/view {:style styles/save-password-checkbox-container}
[profile.components/settings-switch-item
{:label-kw (if can-save-password
:t/save-password
:t/save-password-unavailable)
:active? can-save-password
:value save-password
:action-fn #(re-frame/dispatch [:set-in [:accounts/login :save-password] %])}]])])]
(when processing
[react/view styles/processing-view
[components/activity-indicator {:animating true}]
@ -87,4 +110,4 @@
{:forward? true
:label (i18n/label :t/sign-in)
:disabled? (not (spec/valid? ::db/password password))
:on-press #(login-account @password-text-input address password)}]])]))
:on-press #(login-account @password-text-input address password save-password)}]])]))

View File

@ -71,7 +71,7 @@
(when-not (str/blank? pubkey)
(handlers-macro/merge-fx cofx
(add-account account)
(login.models/login-account normalized-address password)))))
(login.models/login-account normalized-address password false)))))
(defn load-accounts [{:keys [db all-accounts]}]
(let [accounts (->> all-accounts

View File

@ -5,6 +5,7 @@
status-im.protocol.handlers
[status-im.ui.screens.accounts.models :as accounts.models]
status-im.ui.screens.accounts.login.events
[status-im.ui.screens.accounts.login.models :as login]
status-im.ui.screens.accounts.recover.events
[status-im.ui.screens.contacts.events :as contacts]
status-im.ui.screens.add-new.new-chat.events
@ -214,18 +215,17 @@
(re-frame/dispatch [:initialize-app encryption-key]))
:on-accept handle-reset-data})
(defn initialize-views [{{:accounts/keys [accounts] :as db} :db}]
{:db (if (empty? accounts)
(assoc db :view-id :intro :navigation-stack (list :intro))
(let [{:keys [address photo-path name]} (first (sort-by :last-sign-in > (vals accounts)))]
(-> db
(assoc :view-id :login
:navigation-stack (list :login))
(update :accounts/login assoc
:address address
:photo-path photo-path
:name name))))
:handle-initial-push-notification-fx db})
(defn initialize-views [cofx]
(let [{{:accounts/keys [accounts] :as db} :db} cofx
{:keys [address photo-path name]} (first (sort-by :last-sign-in > (vals accounts)))
default-fx {:handle-initial-push-notification-fx db}]
(if (nil? address)
(handlers-macro/merge-fx cofx
default-fx
(navigation/navigate-to-clean :intro))
(handlers-macro/merge-fx cofx
default-fx
(login/open-login address photo-path name)))))
(defn initialize-db
"Initialize db to the initial state"
@ -289,7 +289,8 @@
(fn [{:keys [db] :as cofx} _]
(let [{:transport/keys [chats]} db]
(handlers-macro/merge-fx cofx
{:dispatch [:initialize-keychain]}
{:dispatch [:initialize-keychain]
:clear-user-password [(get-in db [:account/account :address])]}
(navigation/navigate-to-clean nil)
(transport/stop-whisper)))))

View File

@ -13,6 +13,81 @@
(defn- string->js-array [s]
(.parse js/JSON (.-password s)))
;; ********************************************************************************
;; Storing / Retrieving a user password to/from Keychain
;; ********************************************************************************
;;
;; We are using set/get/reset internet credentials there because they are bound
;; to an address (`server`) property.
(defn enum-val [enum-name value-name]
(get-in (js->clj rn/keychain) [enum-name value-name]))
;; We need a more strict access mode for keychain entries that save user password.
;; iOS
;; see this article for more details:
;; https://developer.apple.com/documentation/security/keychain_services/keychain_items/restricting_keychain_item_accessibility?language=objc
(def keychain-restricted-availability
;; From Apple's documentation:
;; > The kSecAttrAccessible attribute enables you to control item availability
;; > relative to the lock state of the device.
;; > It also lets you specify eligibility for restoration to a new device.
;; > If the attribute ends with the string ThisDeviceOnly,
;; > the item can be restored to the same device that created a backup,
;; > but it isnt migrated when restoring another devices backup data.
;; > ...
;; > For extremely sensitive data
;; > THAT YOU NEVER WANT STORED IN iCloud,
;; > you might choose kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly.
;; That is exactly what we use there.
;; Note that the password won't be stored if the device isn't locked by a passcode.
{:accessible (enum-val "ACCESSIBLE" "WHEN_PASSCODE_SET_THIS_DEVICE_ONLY")})
;; Stores the password for the address to the Keychain
(defn save-user-password [address password callback]
(if-not platform/ios?
(callback) ;; no-op on Androids (for now)
(-> (.setInternetCredentials rn/keychain address address password
(clj->js keychain-restricted-availability))
(.then callback))))
(defn handle-callback [callback result]
(if result
(callback (.-password result))
(callback "")))
;; Gets the password for a specified address from the Keychain
(defn get-user-password [address callback]
(if-not platform/ios?
(callback "") ;; no-op on Androids (for now)
(-> (.getInternetCredentials rn/keychain address)
(.then (partial handle-callback callback)))))
;; Clears the password for a specified address from the Keychain
;; (example of usage is logout or signing in w/o "save-password")
(defn clear-user-password [address callback]
(if-not platform/ios?
(callback)
(-> (.resetInternetCredentials rn/keychain address)
(.then callback))))
;; Resolves to `false` if the device doesn't have neither a passcode nor a biometry auth.
(defn can-save-user-password [callback]
(if-not platform/ios?
(callback false)
(-> (.canImplyAuthentication
rn/keychain
(clj->js
{:authenticationType
(enum-val "ACCESS_CONTROL" "BIOMETRY_ANY_OR_DEVICE_PASSCODE")}))
(.then callback))))
;; ********************************************************************************
;; Storing / Retrieving the realm encryption key to/from the Keychain
;; ********************************************************************************
;; Smoke test key to make sure is ok, we noticed some non-random keys on
;; some IOS devices. We check naively that there are no more than key-bytes/2
;; identical characters.
@ -77,3 +152,36 @@
(defn set-username []
(when platform/desktop? (.setUsername rn/keychain username)))
;;;; Effects
(re-frame/reg-fx
:save-user-password
(fn [[address password]]
(save-user-password
address
password
#(when-not %
(log/error
(str "Error while saving password."
" "
"The app will continue to work normally, "
"but you will have to login again next time you launch it."))))))
(re-frame/reg-fx
:get-user-password
(fn [[address callback]]
(get-user-password address callback)))
(re-frame/reg-fx
:clear-user-password
(fn [[address]]
(clear-user-password
address
#(when-not %
(log/error (str "Error while clearing saved password."))))))
(re-frame/reg-fx
:can-save-user-password
(fn [[callback]]
(can-save-user-password callback)))

View File

@ -3,6 +3,17 @@
[status-im.utils.config :as config]
[status-im.ui.screens.accounts.login.models :as models]))
(deftest login-account-internal
(let [initial-db {:db {:node/after-start "something"}}]
(defn test-func [save-password]
(testing (str "login-account-interal test, save password: " save-password)
(let [actual (models/login-account-internal "address" "password" save-password initial-db)]
(testing "it resets :node/after-start"
(is (= (get-in actual [:db :node/after-start]) nil))
(testing "it causes :login effect and preserves save-password flag"
(is (= ["address" "password" save-password] (get-in actual [:login]))))))))
(map test-func [true false])))
(deftest login-account
(let [mainnet-account {:network "mainnet_rpc"
:networks {"mainnet_rpc" {:config {:NetworkId 1}}}}
@ -13,87 +24,109 @@
initial-db {:db {:network "mainnet_rpc"
:accounts/accounts accounts}}]
(testing "status-go has not started"
(let [actual (models/login-account "testnet" "password" initial-db)]
(testing "it starts status-node if it has not started"
(is (= {:NetworkId 3}
(:initialize-geth-fx
actual))))
(testing "it logins the user after the node started"
(is (= [:login-account-internal "testnet" "password"] (get-in actual [:db :node/after-start]))))))
(testing "status-go has started & the user is on mainnet"
(let [db (assoc-in initial-db [:db :status-node-started?] true)
actual (models/login-account "mainnet" "password" db)]
(testing "it does not start status-node if it has already started"
(is (not (:initialize-geth-fx actual))))
(testing "it logs in the user"
(is (= ["mainnet" "password"] (:login actual))))))
(testing "the user has selected a different network"
(testing "status-go has started"
(let [db (assoc-in initial-db [:db :status-node-started?] true)
actual (models/login-account "testnet" "password" db)]
(testing "it dispatches start-node"
(is (get-in actual [:db :node/after-stop] [:start-node "testnet" "password"])))
(testing "it stops status-node"
(is (contains? actual :stop-node)))))
(testing "status-go has not started"
(let [actual (models/login-account "testnet" "password" initial-db)]
(testing "it starts status-node"
(testing "save password"
(testing "save password: status-go not started"
(let [actual (models/login-account "testnet" "password" true initial-db)]
(testing "it starts status-node if it has not started"
(is (= {:NetworkId 3} (:initialize-geth-fx actual))))
(testing "it logins the user after the node started"
(is (= [:login-account-internal "testnet" "password"] (get-in actual [:db :node/after-start])))))))
(is (= [:login-account-internal "testnet" "password" true] (get-in actual [:db :node/after-start]))))))
(testing "status-go has started & the user is on mainnet"
(let [db (assoc-in initial-db [:db :status-node-started?] true)
actual (models/login-account "mainnet" "password" false db)]
(testing "it does not start status-node if it has already started"
(is (not (:initialize-geth-fx actual))))
(testing "it logs in the user"
(is (= ["mainnet" "password" false] (:login actual))))))
(testing "custom bootnodes"
(let [custom-bootnodes {"a" {:id "a"
:name "name-a"
:address "address-a"}
"b" {:id "b"
:name "name-b"
:address "address-b"}}
bootnodes-db (assoc-in
initial-db
[:db :accounts/accounts "mainnet" :bootnodes]
{"mainnet_rpc" custom-bootnodes})]
(testing "the user has selected a different network"
(testing "status-go has started"
(let [db (assoc-in initial-db [:db :status-node-started?] true)
actual (models/login-account "testnet" "password" false db)]
(testing "it dispatches start-node"
(is (get-in actual [:db :node/after-stop] [:start-node "testnet" "password" true])))
(testing "it stops status-node"
(is (contains? actual :stop-node))))))
(testing "custom bootnodes enabled"
(let [bootnodes-enabled-db (assoc-in
bootnodes-db
[:db :accounts/accounts "mainnet" :settings]
{:bootnodes {"mainnet_rpc" true}})
actual (models/login-account "mainnet" "password" bootnodes-enabled-db)]
(testing "status-go has not started"
(let [actual (models/login-account "testnet" "password" false initial-db)]
(testing "it starts status-node if it has not started"
(is (= {:NetworkId 3} (:initialize-geth-fx actual))))
(testing "it logins the user after the node started"
(is (= [:login-account-internal "testnet" "password" false] (get-in actual [:db :node/after-start]))))))
(testing "status-go has started & the user is on mainnet"
(let [db (assoc-in initial-db [:db :status-node-started?] true)
actual (models/login-account "mainnet" "password" false db)]
(testing "it does not start status-node if it has already started"
(is (not (:initialize-geth-fx actual))))
(testing "it logs in the user"
(is (= ["mainnet" "password" false] (:login actual))))))
(testing "the user has selected a different network"
(testing "status-go has started"
(let [db (assoc-in initial-db [:db :status-node-started?] true)
actual (models/login-account "testnet" "password" false db)]
(testing "it dispatches start-node"
(is (get-in actual [:db :node/after-stop] [:start-node "testnet" "password" false])))
(testing "it stops status-node"
(is (contains? actual :stop-node)))))
(testing "status-go has not started"
(let [actual (models/login-account "testnet" "password" false initial-db)]
(testing "it starts status-node"
(is (= {:NetworkId 3} (:initialize-geth-fx actual))))
(testing "it logins the user after the node started"
(is (= [:login-account-internal "testnet" "password" false] (get-in actual [:db :node/after-start])))))))
(testing "custom bootnodes"
(let [custom-bootnodes {"a" {:id "a"
:name "name-a"
:address "address-a"}
"b" {:id "b"
:name "name-b"
:address "address-b"}}
bootnodes-db (assoc-in
initial-db
[:db :accounts/accounts "mainnet" :bootnodes]
{"mainnet_rpc" custom-bootnodes})]
(testing "custom bootnodes enabled"
(let [bootnodes-enabled-db (assoc-in
bootnodes-db
[:db :accounts/accounts "mainnet" :settings]
{:bootnodes {"mainnet_rpc" true}})
actual (models/login-account "mainnet" "password" false bootnodes-enabled-db)]
(testing "status-node has started"
(let [db (assoc-in bootnodes-enabled-db [:db :status-node-started?] true)
actual (models/login-account "mainnet" "password" false db)]
(testing "it dispatches start-node"
(is (get-in actual [:db :node/after-stop] [:start-node "testnet" "password" false])))
(testing "it stops status-node"
(is (contains? actual :stop-node)))))
(testing "status-node has not started"
(let [actual (models/login-account "mainnet" "password" false bootnodes-enabled-db)]
(testing "it adds bootnodes to the config"
(is (= {:ClusterConfig {:Enabled true
:BootNodes ["address-a" "address-b"]}
:NetworkId 1} (:initialize-geth-fx actual))))
(testing "it logins the user after the node started"
(is (= [:login-account-internal "mainnet" "password" false] (get-in actual [:db :node/after-start]))))))))
(testing "custom bootnodes not enabled"
(testing "status-node has started"
(let [db (assoc-in bootnodes-enabled-db [:db :status-node-started?] true)
actual (models/login-account "mainnet" "password" db)]
(testing "it dispatches start-node"
(is (get-in actual [:db :node/after-stop] [:start-node "testnet" "password"])))
(testing "it stops status-node"
(is (contains? actual :stop-node)))))
(let [db (assoc-in bootnodes-db [:db :status-node-started?] true)
actual (models/login-account "mainnet" "password" false db)]
(testing "it does not start status-node if it has already started"
(is (not (:initialize-geth-fx actual))))
(testing "it logs in the user"
(is (= ["mainnet" "password" false] (:login actual))))))
(testing "status-node has not started"
(let [actual (models/login-account "mainnet" "password" bootnodes-enabled-db)]
(testing "it adds bootnodes to the config"
(is (= {:ClusterConfig {:Enabled true
:BootNodes ["address-a" "address-b"]}
:NetworkId 1} (:initialize-geth-fx actual))))
(let [actual (models/login-account "mainnet" "password" false bootnodes-db)]
(testing "it starts status-node without custom bootnodes"
(is (= {:NetworkId 1} (:initialize-geth-fx actual))))
(testing "it logins the user after the node started"
(is (= [:login-account-internal "mainnet" "password"] (get-in actual [:db :node/after-start]))))))))
(testing "custom bootnodes not enabled"
(testing "status-node has started"
(let [db (assoc-in bootnodes-db [:db :status-node-started?] true)
actual (models/login-account "mainnet" "password" db)]
(testing "it does not start status-node if it has already started"
(is (not (:initialize-geth-fx actual))))
(testing "it logs in the user"
(is (= ["mainnet" "password"] (:login actual))))))
(testing "status-node has not started"
(let [actual (models/login-account "mainnet" "password" bootnodes-db)]
(testing "it starts status-node without custom bootnodes"
(is (= {:NetworkId 1} (:initialize-geth-fx actual))))
(testing "it logins the user after the node started"
(is (= [:login-account-internal "mainnet" "password"] (get-in actual [:db :node/after-start])))))))))))
(is (= [:login-account-internal "mainnet" "password" false] (get-in actual [:db :node/after-start]))))))))))))
(deftest restart-node?
(testing "custom bootnodes is toggled off"