[#15759] - Add onboarding recovery phrase screen
This commit is contained in:
parent
a4bc18ee3f
commit
c62204121d
|
@ -42,7 +42,8 @@
|
||||||
(case type
|
(case type
|
||||||
:words
|
:words
|
||||||
[rn/flat-list
|
[rn/flat-list
|
||||||
{:data words
|
{:keyboard-should-persist-taps :always
|
||||||
|
:data words
|
||||||
:content-container-style style/word-list
|
:content-container-style style/word-list
|
||||||
:render-fn word-component
|
:render-fn word-component
|
||||||
:render-data {:blur? blur?
|
:render-data {:blur? blur?
|
||||||
|
|
|
@ -29,7 +29,8 @@
|
||||||
(h/test "Marked when words doesn't satisfy a predicate"
|
(h/test "Marked when words doesn't satisfy a predicate"
|
||||||
(h/render [recovery-phrase/recovery-phrase-input
|
(h/render [recovery-phrase/recovery-phrase-input
|
||||||
{:mark-errors? true
|
{:mark-errors? true
|
||||||
:error-pred #(>= (count %) 5)}
|
:error-pred-current-word #(>= (count %) 5)
|
||||||
|
:error-pred-written-words #(>= (count %) 5)}
|
||||||
"Text with some error words that don't satisfy the predicate"])
|
"Text with some error words that don't satisfy the predicate"])
|
||||||
(let [children-text-nodes (-> (h/get-by-label-text :recovery-phrase-input)
|
(let [children-text-nodes (-> (h/get-by-label-text :recovery-phrase-input)
|
||||||
(oops/oget "props" "children" "props" "children")
|
(oops/oget "props" "children" "props" "children")
|
||||||
|
|
|
@ -15,15 +15,27 @@
|
||||||
text])
|
text])
|
||||||
|
|
||||||
(defn- mark-error-words
|
(defn- mark-error-words
|
||||||
[pred text word-limit]
|
[pred-last-word pred-previous-words text word-limit]
|
||||||
(let [word-limit (or word-limit ##Inf)]
|
(let [last-index (dec (count (string/split text #"\s+")))
|
||||||
(into [:<>]
|
words (map #(apply str %)
|
||||||
(comp (map-indexed (fn [idx word]
|
(partition-by #(= " " %) text))]
|
||||||
(if (or (pred word) (>= idx word-limit))
|
(->> 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]
|
[error-word word]
|
||||||
word)))
|
word)))))
|
||||||
(interpose " "))
|
{:result [:<>]
|
||||||
(string/split text #" "))))
|
:idx 0})
|
||||||
|
:result)))
|
||||||
|
|
||||||
(defn recovery-phrase-input
|
(defn recovery-phrase-input
|
||||||
[_ _]
|
[_ _]
|
||||||
|
@ -31,9 +43,11 @@
|
||||||
set-focused #(reset! state :focused)
|
set-focused #(reset! state :focused)
|
||||||
set-default #(reset! state :default)]
|
set-default #(reset! state :default)]
|
||||||
(fn [{:keys [customization-color override-theme blur? on-focus on-blur mark-errors?
|
(fn [{:keys [customization-color override-theme blur? on-focus on-blur mark-errors?
|
||||||
error-pred word-limit]
|
error-pred-current-word error-pred-written-words word-limit]
|
||||||
:or {customization-color :blue
|
:or {customization-color :blue
|
||||||
error-pred (constantly false)}
|
word-limit ##Inf
|
||||||
|
error-pred-current-word (constantly false)
|
||||||
|
error-pred-written-words (constantly false)}
|
||||||
:as props}
|
:as props}
|
||||||
text]
|
text]
|
||||||
(let [extra-props (apply dissoc props custom-props)]
|
(let [extra-props (apply dissoc props custom-props)]
|
||||||
|
@ -53,5 +67,5 @@
|
||||||
(when on-blur (on-blur)))}
|
(when on-blur (on-blur)))}
|
||||||
extra-props)
|
extra-props)
|
||||||
(if mark-errors?
|
(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)]]))))
|
text)]]))))
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
(ns status-im2.contexts.onboarding.enter-seed-phrase.style
|
(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
|
(def page-container
|
||||||
{:position :absolute
|
{:position :absolute
|
||||||
|
@ -9,6 +12,27 @@
|
||||||
:right 0
|
:right 0
|
||||||
:background-color colors/neutral-80-opa-80-blur})
|
: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
|
(def input-container
|
||||||
{:height 120
|
{:height 120
|
||||||
|
:margin-top 12
|
||||||
:margin-horizontal -20})
|
: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
|
(ns status-im2.contexts.onboarding.enter-seed-phrase.view
|
||||||
(:require [clojure.string :as string]
|
(:require [clojure.string :as string]
|
||||||
[quo2.core :as quo]
|
[quo2.core :as quo]
|
||||||
|
[quo2.foundations.colors :as colors]
|
||||||
[react-native.core :as rn]
|
[react-native.core :as rn]
|
||||||
[react-native.safe-area :as safe-area]
|
[react-native.safe-area :as safe-area]
|
||||||
[reagent.core :as reagent]
|
[reagent.core :as reagent]
|
||||||
|
@ -13,57 +14,152 @@
|
||||||
[utils.re-frame :as rf]
|
[utils.re-frame :as rf]
|
||||||
[utils.security.core :as security]))
|
[utils.security.core :as security]))
|
||||||
|
|
||||||
(def button-disabled?
|
(def ^:private max-seed-phrase-length
|
||||||
(comp not constants/seed-phrase-valid-length mnemonic/words-count))
|
(apply max constants/seed-phrase-valid-length))
|
||||||
|
|
||||||
(defn clean-seed-phrase
|
(defn- partial-word-in-dictionary?
|
||||||
[s]
|
[partial-word]
|
||||||
(as-> s $
|
(some #(string/starts-with? % partial-word) mnemonic/dictionary))
|
||||||
(string/lower-case $)
|
|
||||||
(string/split $ #"\s")
|
|
||||||
(filter #(not (string/blank? %)) $)
|
|
||||||
(string/join " " $)))
|
|
||||||
|
|
||||||
(defn page
|
(defn- word-in-dictionary?
|
||||||
[{:keys [navigation-bar-top]}]
|
[word]
|
||||||
(let [seed-phrase (reagent/atom "")
|
(some #(= % word) mnemonic/dictionary))
|
||||||
error-message (reagent/atom "")
|
|
||||||
on-invalid-seed-phrase #(reset! error-message (i18n/label :t/custom-seed-phrase))]
|
(def ^:private partial-word-not-in-dictionary?
|
||||||
(fn []
|
(comp not partial-word-in-dictionary?))
|
||||||
[rn/view {:style style/page-container}
|
|
||||||
[navigation-bar/navigation-bar {:top navigation-bar-top}]
|
(def ^:private word-not-in-dictionary?
|
||||||
[rn/view {:style {:padding-horizontal 20}}
|
(comp not word-in-dictionary?))
|
||||||
[quo/text
|
|
||||||
{:weight :bold
|
(defn- header
|
||||||
:align :center}
|
[seed-phrase-count]
|
||||||
|
[rn/view {:style style/header-container}
|
||||||
|
[quo/text {:weight :semi-bold :size :heading-1}
|
||||||
(i18n/label :t/use-recovery-phrase)]
|
(i18n/label :t/use-recovery-phrase)]
|
||||||
|
[rn/view {:style style/word-count-container}
|
||||||
[quo/text
|
[quo/text
|
||||||
(i18n/label-pluralize (mnemonic/words-count @seed-phrase) :t/words-n)]
|
{: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}
|
[rn/view {:style style/input-container}
|
||||||
[quo/recovery-phrase-input
|
[quo/recovery-phrase-input
|
||||||
{:on-change-text (fn [t]
|
{:accessibility-label :passphrase-input
|
||||||
(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)
|
:placeholder (i18n/label :t/seed-phrase-placeholder)
|
||||||
:auto-correct false}
|
:auto-capitalize :none
|
||||||
@seed-phrase]]
|
: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
|
[quo/button
|
||||||
{:disabled (button-disabled? @seed-phrase)
|
{:style (style/continue-button keyboard-shown?)
|
||||||
:on-press #(rf/dispatch [:onboarding-2/seed-phrase-entered
|
: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)
|
(security/mask-data @seed-phrase)
|
||||||
on-invalid-seed-phrase])}
|
set-invalid-seed-phrase]))]
|
||||||
(i18n/label :t/continue)]
|
(let [words-coll (mnemonic/passphrase->words @seed-phrase)
|
||||||
(when (seq @error-message)
|
last-word (peek words-coll)
|
||||||
[quo/text @error-message])]])))
|
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
|
(defn enter-seed-phrase
|
||||||
[]
|
[]
|
||||||
(let [{:keys [top]} (safe-area/get-insets)]
|
(let [{navigation-bar-top :top} (safe-area/get-insets)]
|
||||||
[rn/view {:style {:flex 1}}
|
[rn/view {:style style/full-layout}
|
||||||
[background/view true]
|
[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",
|
"with-full-encryption": "With full metadata privacy and e2e encryption",
|
||||||
"fetch-community": "Fetch community",
|
"fetch-community": "Fetch community",
|
||||||
"fetching-community": "Fetching 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-count": "Word count",
|
||||||
"word-n": "Word #{{number}}",
|
"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.",
|
"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