[#15759] - Add onboarding recovery phrase screen

This commit is contained in:
Ulises Manuel Cárdenas 2023-06-01 05:23:07 -06:00 committed by GitHub
parent a4bc18ee3f
commit c62204121d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 207 additions and 66 deletions

View File

@ -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?

View File

@ -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")

View File

@ -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)]]))))

View File

@ -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})

View File

@ -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]]]))

View File

@ -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.",