[#15759] - Add onboarding recovery phrase screen
This commit is contained in:
parent
a4bc18ee3f
commit
c62204121d
|
@ -42,7 +42,8 @@
|
|||
(case type
|
||||
:words
|
||||
[rn/flat-list
|
||||
{:data words
|
||||
{:keyboard-should-persist-taps :always
|
||||
:data words
|
||||
:content-container-style style/word-list
|
||||
:render-fn word-component
|
||||
:render-data {:blur? blur?
|
||||
|
|
|
@ -28,8 +28,9 @@
|
|||
(h/describe "Error text"
|
||||
(h/test "Marked when words doesn't satisfy a predicate"
|
||||
(h/render [recovery-phrase/recovery-phrase-input
|
||||
{:mark-errors? true
|
||||
:error-pred #(>= (count %) 5)}
|
||||
{:mark-errors? true
|
||||
:error-pred-current-word #(>= (count %) 5)
|
||||
:error-pred-written-words #(>= (count %) 5)}
|
||||
"Text with some error words that don't satisfy the predicate"])
|
||||
(let [children-text-nodes (-> (h/get-by-label-text :recovery-phrase-input)
|
||||
(oops/oget "props" "children" "props" "children")
|
||||
|
|
|
@ -15,15 +15,27 @@
|
|||
text])
|
||||
|
||||
(defn- mark-error-words
|
||||
[pred text word-limit]
|
||||
(let [word-limit (or word-limit ##Inf)]
|
||||
(into [:<>]
|
||||
(comp (map-indexed (fn [idx word]
|
||||
(if (or (pred word) (>= idx word-limit))
|
||||
[error-word word]
|
||||
word)))
|
||||
(interpose " "))
|
||||
(string/split text #" "))))
|
||||
[pred-last-word pred-previous-words text word-limit]
|
||||
(let [last-index (dec (count (string/split text #"\s+")))
|
||||
words (map #(apply str %)
|
||||
(partition-by #(= " " %) text))]
|
||||
(->> words
|
||||
(reduce (fn [{:keys [idx] :as acc} word]
|
||||
(let [error-pred (if (= last-index idx) pred-last-word pred-previous-words)
|
||||
invalid-word? (and (or (error-pred word)
|
||||
(>= idx word-limit))
|
||||
(not (string/blank? word)))
|
||||
not-blank-spaces? (not (string/blank? word))]
|
||||
(cond-> acc
|
||||
not-blank-spaces? (update :idx inc)
|
||||
:always (update :result
|
||||
conj
|
||||
(if invalid-word?
|
||||
[error-word word]
|
||||
word)))))
|
||||
{:result [:<>]
|
||||
:idx 0})
|
||||
:result)))
|
||||
|
||||
(defn recovery-phrase-input
|
||||
[_ _]
|
||||
|
@ -31,9 +43,11 @@
|
|||
set-focused #(reset! state :focused)
|
||||
set-default #(reset! state :default)]
|
||||
(fn [{:keys [customization-color override-theme blur? on-focus on-blur mark-errors?
|
||||
error-pred word-limit]
|
||||
:or {customization-color :blue
|
||||
error-pred (constantly false)}
|
||||
error-pred-current-word error-pred-written-words word-limit]
|
||||
:or {customization-color :blue
|
||||
word-limit ##Inf
|
||||
error-pred-current-word (constantly false)
|
||||
error-pred-written-words (constantly false)}
|
||||
:as props}
|
||||
text]
|
||||
(let [extra-props (apply dissoc props custom-props)]
|
||||
|
@ -53,5 +67,5 @@
|
|||
(when on-blur (on-blur)))}
|
||||
extra-props)
|
||||
(if mark-errors?
|
||||
(mark-error-words error-pred text word-limit)
|
||||
(mark-error-words error-pred-current-word error-pred-written-words text word-limit)
|
||||
text)]]))))
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
(ns status-im2.contexts.onboarding.enter-seed-phrase.style
|
||||
(:require [quo2.foundations.colors :as colors]))
|
||||
(:require [quo2.foundations.colors :as colors]
|
||||
[react-native.safe-area :as safe-area]))
|
||||
|
||||
(def full-layout {:flex 1})
|
||||
|
||||
(def page-container
|
||||
{:position :absolute
|
||||
|
@ -9,6 +12,27 @@
|
|||
:right 0
|
||||
:background-color colors/neutral-80-opa-80-blur})
|
||||
|
||||
(def form-container
|
||||
{:flex 1
|
||||
:padding-horizontal 20
|
||||
:padding-vertical 12})
|
||||
|
||||
(def header-container
|
||||
{:flex-direction :row
|
||||
:justify-content :space-between})
|
||||
|
||||
(def word-count-container
|
||||
{:justify-content :flex-end
|
||||
:margin-bottom 2})
|
||||
|
||||
(def input-container
|
||||
{:height 120
|
||||
:margin-top 12
|
||||
:margin-horizontal -20})
|
||||
|
||||
(defn continue-button
|
||||
[keyboard-shown?]
|
||||
{:margin-top :auto
|
||||
:margin-bottom (when-not keyboard-shown? (safe-area/get-bottom))})
|
||||
|
||||
(def keyboard-container {:margin-top :auto})
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
(ns status-im2.contexts.onboarding.enter-seed-phrase.view
|
||||
(:require [clojure.string :as string]
|
||||
[quo2.core :as quo]
|
||||
[quo2.foundations.colors :as colors]
|
||||
[react-native.core :as rn]
|
||||
[react-native.safe-area :as safe-area]
|
||||
[reagent.core :as reagent]
|
||||
|
@ -13,57 +14,152 @@
|
|||
[utils.re-frame :as rf]
|
||||
[utils.security.core :as security]))
|
||||
|
||||
(def button-disabled?
|
||||
(comp not constants/seed-phrase-valid-length mnemonic/words-count))
|
||||
(def ^:private max-seed-phrase-length
|
||||
(apply max constants/seed-phrase-valid-length))
|
||||
|
||||
(defn clean-seed-phrase
|
||||
[s]
|
||||
(as-> s $
|
||||
(string/lower-case $)
|
||||
(string/split $ #"\s")
|
||||
(filter #(not (string/blank? %)) $)
|
||||
(string/join " " $)))
|
||||
(defn- partial-word-in-dictionary?
|
||||
[partial-word]
|
||||
(some #(string/starts-with? % partial-word) mnemonic/dictionary))
|
||||
|
||||
(defn page
|
||||
[{:keys [navigation-bar-top]}]
|
||||
(let [seed-phrase (reagent/atom "")
|
||||
error-message (reagent/atom "")
|
||||
on-invalid-seed-phrase #(reset! error-message (i18n/label :t/custom-seed-phrase))]
|
||||
(fn []
|
||||
[rn/view {:style style/page-container}
|
||||
[navigation-bar/navigation-bar {:top navigation-bar-top}]
|
||||
[rn/view {:style {:padding-horizontal 20}}
|
||||
[quo/text
|
||||
{:weight :bold
|
||||
:align :center}
|
||||
(i18n/label :t/use-recovery-phrase)]
|
||||
[quo/text
|
||||
(i18n/label-pluralize (mnemonic/words-count @seed-phrase) :t/words-n)]
|
||||
[rn/view {:style style/input-container}
|
||||
[quo/recovery-phrase-input
|
||||
{:on-change-text (fn [t]
|
||||
(reset! seed-phrase (clean-seed-phrase t))
|
||||
(reset! error-message ""))
|
||||
:mark-errors? true
|
||||
:error-pred (constantly false)
|
||||
:word-limit 24
|
||||
:auto-focus true
|
||||
:accessibility-label :passphrase-input
|
||||
:placeholder (i18n/label :t/seed-phrase-placeholder)
|
||||
:auto-correct false}
|
||||
@seed-phrase]]
|
||||
[quo/button
|
||||
{:disabled (button-disabled? @seed-phrase)
|
||||
:on-press #(rf/dispatch [:onboarding-2/seed-phrase-entered
|
||||
(security/mask-data @seed-phrase)
|
||||
on-invalid-seed-phrase])}
|
||||
(i18n/label :t/continue)]
|
||||
(when (seq @error-message)
|
||||
[quo/text @error-message])]])))
|
||||
(defn- word-in-dictionary?
|
||||
[word]
|
||||
(some #(= % word) mnemonic/dictionary))
|
||||
|
||||
(def ^:private partial-word-not-in-dictionary?
|
||||
(comp not partial-word-in-dictionary?))
|
||||
|
||||
(def ^:private word-not-in-dictionary?
|
||||
(comp not word-in-dictionary?))
|
||||
|
||||
(defn- header
|
||||
[seed-phrase-count]
|
||||
[rn/view {:style style/header-container}
|
||||
[quo/text {:weight :semi-bold :size :heading-1}
|
||||
(i18n/label :t/use-recovery-phrase)]
|
||||
[rn/view {:style style/word-count-container}
|
||||
[quo/text
|
||||
{:style {:color colors/white-opa-40}
|
||||
:weight :regular
|
||||
:size :paragraph-2}
|
||||
(i18n/label-pluralize seed-phrase-count :t/words-n)]]])
|
||||
|
||||
(defn- clean-seed-phrase
|
||||
[seed-phrase]
|
||||
(-> seed-phrase
|
||||
(string/lower-case)
|
||||
(string/replace #", " " ")
|
||||
(string/replace #"," " ")
|
||||
(string/replace #"\s+" " ")
|
||||
(string/trim)))
|
||||
|
||||
(defn- recovery-form
|
||||
[{:keys [seed-phrase word-count error-state? all-words-valid? on-change-seed-phrase
|
||||
keyboard-shown? on-submit]}]
|
||||
(let [button-disabled? (or error-state?
|
||||
(not (constants/seed-phrase-valid-length word-count))
|
||||
(not all-words-valid?))]
|
||||
[rn/view {:style style/form-container}
|
||||
[header word-count]
|
||||
[rn/view {:style style/input-container}
|
||||
[quo/recovery-phrase-input
|
||||
{:accessibility-label :passphrase-input
|
||||
:placeholder (i18n/label :t/seed-phrase-placeholder)
|
||||
:auto-capitalize :none
|
||||
:auto-correct false
|
||||
:auto-focus true
|
||||
:mark-errors? true
|
||||
:word-limit max-seed-phrase-length
|
||||
:error-pred-current-word partial-word-not-in-dictionary?
|
||||
:error-pred-written-words word-not-in-dictionary?
|
||||
:on-change-text on-change-seed-phrase}
|
||||
seed-phrase]]
|
||||
[quo/button
|
||||
{:style (style/continue-button keyboard-shown?)
|
||||
:disabled button-disabled?
|
||||
:on-press on-submit}
|
||||
(i18n/label :t/continue)]]))
|
||||
|
||||
(defn keyboard-suggestions
|
||||
[current-word]
|
||||
(->> mnemonic/dictionary
|
||||
(filter #(string/starts-with? % current-word))
|
||||
(take 7)))
|
||||
|
||||
(defn screen
|
||||
[]
|
||||
(reagent/with-let [keyboard-shown? (reagent/atom false)
|
||||
keyboard-show-listener (.addListener rn/keyboard
|
||||
"keyboardDidShow"
|
||||
#(reset! keyboard-shown? true))
|
||||
keyboard-hide-listener (.addListener rn/keyboard
|
||||
"keyboardDidHide"
|
||||
#(reset! keyboard-shown? false))
|
||||
invalid-seed-phrase? (reagent/atom false)
|
||||
set-invalid-seed-phrase #(reset! invalid-seed-phrase? true)
|
||||
seed-phrase (reagent/atom "")
|
||||
on-change-seed-phrase (fn [new-phrase]
|
||||
(when @invalid-seed-phrase?
|
||||
(reset! invalid-seed-phrase? false))
|
||||
(reset! seed-phrase new-phrase))
|
||||
on-submit (fn []
|
||||
(swap! seed-phrase clean-seed-phrase)
|
||||
(rf/dispatch [:onboarding-2/seed-phrase-entered
|
||||
(security/mask-data @seed-phrase)
|
||||
set-invalid-seed-phrase]))]
|
||||
(let [words-coll (mnemonic/passphrase->words @seed-phrase)
|
||||
last-word (peek words-coll)
|
||||
pick-suggested-word (fn [pressed-word]
|
||||
(swap! seed-phrase str (subs pressed-word (count last-word)) " "))
|
||||
;; Last word doesn't exist in dictionary while being written by the user, so
|
||||
;; it's validated checking whether is a substring of a dictionary word or not.
|
||||
last-partial-word-valid? (partial-word-in-dictionary? last-word)
|
||||
last-word-valid? (word-in-dictionary? last-word)
|
||||
butlast-words-valid? (every? word-in-dictionary? (butlast words-coll))
|
||||
all-words-valid? (and butlast-words-valid? last-word-valid?)
|
||||
word-count (mnemonic/words-count @seed-phrase)
|
||||
words-exceeded? (> word-count max-seed-phrase-length)
|
||||
error-in-words? (or (not last-partial-word-valid?)
|
||||
(not butlast-words-valid?))
|
||||
upper-case? (boolean (re-find #"[A-Z]" @seed-phrase))
|
||||
suggestions-state (cond
|
||||
(or error-in-words?
|
||||
words-exceeded?
|
||||
@invalid-seed-phrase?) :error
|
||||
(string/blank? @seed-phrase) :info
|
||||
(string/ends-with? @seed-phrase " ") :empty
|
||||
:else :words)
|
||||
suggestions-text (cond
|
||||
upper-case? (i18n/label :t/seed-phrase-words-uppercase)
|
||||
words-exceeded? (i18n/label :t/seed-phrase-words-exceeded)
|
||||
error-in-words? (i18n/label :t/seed-phrase-error)
|
||||
@invalid-seed-phrase? (i18n/label :t/seed-phrase-invalid)
|
||||
:else (i18n/label :t/seed-phrase-info))]
|
||||
[:<>
|
||||
[recovery-form
|
||||
{:seed-phrase @seed-phrase
|
||||
:error-state? (= suggestions-state :error)
|
||||
:all-words-valid? all-words-valid?
|
||||
:on-change-seed-phrase on-change-seed-phrase
|
||||
:word-count word-count
|
||||
:on-submit on-submit
|
||||
:keyboard-shown? @keyboard-shown?}]
|
||||
(when @keyboard-shown?
|
||||
[rn/view {:style style/keyboard-container}
|
||||
[quo/predictive-keyboard
|
||||
{:type suggestions-state
|
||||
:blur? true
|
||||
:text suggestions-text
|
||||
:words (keyboard-suggestions last-word)
|
||||
:on-press pick-suggested-word}]])])
|
||||
(finally
|
||||
(.remove keyboard-show-listener)
|
||||
(.remove keyboard-hide-listener))))
|
||||
|
||||
(defn enter-seed-phrase
|
||||
[]
|
||||
(let [{:keys [top]} (safe-area/get-insets)]
|
||||
[rn/view {:style {:flex 1}}
|
||||
(let [{navigation-bar-top :top} (safe-area/get-insets)]
|
||||
[rn/view {:style style/full-layout}
|
||||
[background/view true]
|
||||
[page {:navigation-bar-top top}]]))
|
||||
[rn/keyboard-avoiding-view {:style style/page-container}
|
||||
[navigation-bar/navigation-bar {:top navigation-bar-top}]
|
||||
[screen]]]))
|
||||
|
|
|
@ -1489,7 +1489,12 @@
|
|||
"with-full-encryption": "With full metadata privacy and e2e encryption",
|
||||
"fetch-community": "Fetch community",
|
||||
"fetching-community": "Fetching community...",
|
||||
"seed-phrase-placeholder": "Seed phrase...",
|
||||
"seed-phrase-placeholder": "Type or paste your recovery phrase",
|
||||
"seed-phrase-words-exceeded": "Recovery phrase cannot exceed 24 words",
|
||||
"seed-phrase-words-uppercase": "Recovery phrase cannot contain uppercase characters",
|
||||
"seed-phrase-error": "Recovery phrase contains invalid words",
|
||||
"seed-phrase-invalid": "Invalid recovery phrase",
|
||||
"seed-phrase-info": "Enter 12, 18 or 24 words separated by spaces",
|
||||
"word-count": "Word count",
|
||||
"word-n": "Word #{{number}}",
|
||||
"word-n-description": "In order to check if you have backed up your seed phrase correctly, enter the word #{{number}} above.",
|
||||
|
|
Loading…
Reference in New Issue