Add validation for when adding a contact (#15192)

This commit is contained in:
erikseppanen 2023-03-22 13:13:56 -04:00 committed by GitHub
parent a502da6ea4
commit c238ebe36e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 338 additions and 183 deletions

View File

@ -1,5 +1,6 @@
(ns status-im.mobile-sync-settings.core
(:require [status-im2.common.bottom-sheet.events :as bottom-sheet]
[status-im2.contexts.add-new-contact.events :as add-new-contact]
[status-im.mailserver.core :as mailserver]
[status-im.multiaccounts.model :as multiaccounts.model]
[status-im.multiaccounts.update.core :as multiaccounts.update]
@ -42,7 +43,8 @@
(and logged-in? initialized?)
[(mailserver/process-next-messages-request)
(bottom-sheet/hide-bottom-sheet)
(wallet/restart-wallet-service nil)]
(wallet/restart-wallet-service nil)
(add-new-contact/set-new-identity-reconnected)]
logged-in?
[(mailserver/process-next-messages-request)

View File

@ -1,5 +1,6 @@
(ns status-im2.contexts.add-new-contact.events
(:require [utils.re-frame :as rf]
(:require [clojure.string :as string]
[utils.re-frame :as rf]
[status-im.utils.types :as types]
[re-frame.core :as re-frame]
[status-im.ethereum.core :as ethereum]
@ -11,11 +12,120 @@
[status-im2.contexts.contacts.events :as data-store.contacts]
[status-im.utils.utils :as utils]))
(defn init-contact
"Create a new contact (persisted to app-db as [:contacts/new-identity]).
The following options are available:
| key | description |
| -------------------|-------------|
| `:user-public-key` | user's public key (not the contact)
| `:input` | raw user input (untrimmed)
| `:scanned` | scanned user input (untrimmed)
| `:id` | public-key|compressed-key|ens
| `:type` | :empty|:public-key|:compressed-key|:ens
| `:ens` | id.eth|id.ens-stateofus
| `:public-key` | public-key (from decompression or ens resolution)
| `:state` | :empty|:invalid|:decompress-key|:resolve-ens|:valid
| `:msg` | keyword i18n msg"
([]
(-> [:user-public-key :input :scanned :id :type :ens :public-key :state :msg]
(zipmap (repeat nil))))
([kv] (-> (init-contact) (merge kv))))
(def url-regex #"^https?://join.status.im/u/(.+)")
(defn ->id
[{:keys [input] :as contact}]
(let [trimmed-input (utils/safe-trim input)]
(->> {:id (if (empty? trimmed-input)
nil
(if-some [[_ id] (re-matches url-regex trimmed-input)]
id
trimmed-input))}
(merge contact))))
(defn ->type
[{:keys [id] :as contact}]
(->> (cond
(empty? id)
{:type :empty}
(validators/valid-public-key? id)
{:type :public-key
:public-key id}
(validators/valid-compressed-key? id)
{:type :compressed-key}
:else
{:type :ens
:ens (stateofus/ens-name-parse id)})
(merge contact)))
(defn ->state
[{:keys [id type public-key user-public-key] :as contact}]
(->> (cond
(empty? id)
{:state :empty}
(= type :public-key)
{:state :invalid
:msg :t/not-a-chatkey}
(= public-key user-public-key)
{:state :invalid
:msg :t/can-not-add-yourself}
(and (= type :compressed-key) (empty? public-key))
{:state :decompress-key}
(and (= type :ens) (empty? public-key))
{:state :resolve-ens}
(and (or (= type :compressed-key) (= type :ens))
(validators/valid-public-key? public-key))
{:state :valid})
(merge contact)))
(def validate-contact (comp ->state ->type ->id))
(defn dispatcher [event input] (fn [arg] (rf/dispatch [event input arg])))
(rf/defn set-new-identity
{:events [:contacts/set-new-identity]}
[{:keys [db]} input scanned]
(let [user-public-key (get-in db [:multiaccount :public-key])
{:keys [input id ens state]
:as contact} (-> {:user-public-key user-public-key
:input input
:scanned scanned}
init-contact
validate-contact)]
(case state
:empty {:db (dissoc db :contacts/new-identity)}
(:valid :invalid) {:db (assoc db :contacts/new-identity contact)}
:decompress-key {:db (assoc db :contacts/new-identity contact)
:contacts/decompress-public-key
{:compressed-key id
:on-success
(dispatcher :contacts/set-new-identity-success input)
:on-error
(dispatcher :contacts/set-new-identity-error input)}}
:resolve-ens {:db (assoc db :contacts/new-identity contact)
:contacts/resolve-public-key-from-ens
{:chain-id (ethereum/chain-id db)
:ens ens
:on-success
(dispatcher :contacts/set-new-identity-success input)
:on-error
(dispatcher :contacts/set-new-identity-error input)}})))
(re-frame/reg-fx
:contacts/decompress-public-key
(fn [{:keys [public-key on-success on-error]}]
(fn [{:keys [compressed-key on-success on-error]}]
(status/compressed-key->public-key
public-key
compressed-key
(fn [resp]
(let [{:keys [error]} (types/json->clj resp)]
(if error
@ -23,74 +133,16 @@
(on-success (str "0x" (subs resp 5)))))))))
(re-frame/reg-fx
:contacts/resolve-public-key-from-ens-name
(fn [{:keys [chain-id ens-name on-success on-error]}]
(ens/pubkey chain-id ens-name on-success on-error)))
(defn fx-callbacks
[input ens-name]
{:on-success (fn [pubkey]
(rf/dispatch [:contacts/set-new-identity-success input ens-name pubkey]))
:on-error (fn [err]
(rf/dispatch [:contacts/set-new-identity-error err input]))})
(defn identify-type
[input]
(let [regex #"^https?://join.status.im/u/(.+)"
id (as-> (utils/safe-trim input) $
(if-some [[_ match] (re-matches regex $)]
match
$)
(if (empty? $) nil $))
public-key? (validators/valid-public-key? id)
compressed-key? (validators/valid-compressed-key? id)
type (cond (empty? id) :empty
public-key? :public-key
compressed-key? :compressed-key
:else :ens-name)
ens-name (when (= type :ens-name)
(stateofus/ens-name-parse id))]
{:input input
:id id
:type type
:ens-name ens-name}))
(rf/defn set-new-identity
{:events [:contacts/set-new-identity]}
[{:keys [db]} input]
(let [{:keys [input id type ens-name]} (identify-type input)]
(case type
:empty {:db (dissoc db :contacts/new-identity)}
:public-key {:db (assoc db
:contacts/new-identity
{:input input
:public-key id
:state :error
:error :uncompressed-key})}
:compressed-key {:db
(assoc db
:contacts/new-identity
{:input input
:state :searching})
:contacts/decompress-public-key
(merge {:public-key id}
(fx-callbacks id ens-name))}
:ens-name {:db
(assoc db
:contacts/new-identity
{:input input
:state :searching})
:contacts/resolve-public-key-from-ens-name
(merge {:chain-id (ethereum/chain-id db)
:ens-name ens-name}
(fx-callbacks id ens-name))})))
:contacts/resolve-public-key-from-ens
(fn [{:keys [chain-id ens on-success on-error]}]
(ens/pubkey chain-id ens on-success on-error)))
(rf/defn build-contact
{:events [:contacts/build-contact]}
[_ pubkey ens-name open-profile-modal?]
[_ pubkey ens open-profile-modal?]
{:json-rpc/call [{:method "wakuext_buildContact"
:params [{:publicKey pubkey
:ENSName ens-name}]
:ENSName ens}]
:js-response true
:on-success #(rf/dispatch [:contacts/contact-built
pubkey
@ -106,24 +158,25 @@
(rf/defn set-new-identity-success
{:events [:contacts/set-new-identity-success]}
[{:keys [db] :as cofx} input ens-name pubkey]
(rf/merge cofx
{:db (assoc db
:contacts/new-identity
{:input input
:public-key pubkey
:ens-name ens-name
:state :valid})}
(build-contact pubkey ens-name false)))
[{:keys [db]} input pubkey]
(let [contact (get-in db [:contacts/new-identity])]
(when (= (:input contact) input)
(rf/merge {:db (assoc db
:contacts/new-identity
(->state (assoc contact :public-key pubkey)))}
(build-contact pubkey (:ens contact) false)))))
(rf/defn set-new-identity-error
{:events [:contacts/set-new-identity-error]}
[{:keys [db]} error input]
{:db (assoc db
:contacts/new-identity
{:input input
:state :error
:error :invalid})})
[{:keys [db]} input err]
(let [contact (get-in db [:contacts/new-identity])]
(when (= (:input contact) input)
(let [state (cond
(or (string/includes? (:message err) "fallback failed")
(string/includes? (:message err) "no such host"))
{:state :invalid :msg :t/lost-connection}
:else {:state :invalid})]
{:db (assoc db :contacts/new-identity (merge contact state))}))))
(rf/defn clear-new-identity
{:events [:contacts/clear-new-identity :contacts/new-chat-focus]}
@ -132,7 +185,13 @@
(rf/defn qr-code-scanned
{:events [:contacts/qr-code-scanned]}
[{:keys [db] :as cofx} input]
[{:keys [db] :as cofx} scanned]
(rf/merge cofx
(set-new-identity input)
(set-new-identity scanned scanned)
(navigation/navigate-back)))
(rf/defn set-new-identity-reconnected
[{:keys [db]}]
(let [input (get-in db [:contacts/new-identity :input])
resubmit? (and input (= :new-contact (get-in db [:view-id])))]
(rf/dispatch [:contacts/set-new-identity input])))

View File

@ -1,7 +1,10 @@
(ns status-im2.contexts.add-new-contact.events-test
(:require [cljs.test :refer-macros [deftest is are]]
[status-im2.contexts.add-new-contact.events :as core]))
(:require [cljs.test :refer-macros [deftest are]]
[status-im2.contexts.add-new-contact.events :as events]))
(def user-ukey
"0x04ca27ed9c7c4099d230c6d8853ad0cfaf084a019c543e9e433d3c04fac6de9147cf572b10e247cfe52f396b5aa10456b56dd1cf1d8a681e2b93993d44594b2e85")
(def user-ckey "zQ3shtFEo4PxpQiYGcNZZ8xhJmhD6WBXwnHPBueu5SRnvPXjk")
(def ukey
"0x045596a7ff87da36860a84b0908191ce60a504afc94aac93c1abd774f182967ce694f1bf2d8773cd59f4dd0863e951f9b7f7351c5516291a0fceb73f8c392a0e88")
(def ckey "zQ3shWj4WaBdf2zYKCkXe6PHxDxNTzZyid1i75879Ue9cX9gA")
@ -10,47 +13,118 @@
(def link-ckey (str "https://join.status.im/u/" ckey))
(def link-ens (str "https://join.status.im/u/" ens))
(deftest identify-type-test
(are [input expected] (= (core/identify-type input) expected)
"" {:input ""
:id nil
:type :empty
:ens-name nil}
;;; unit tests (no app-db involved)
ukey {:input ukey
:id ukey
:type :public-key
:ens-name nil}
(deftest validate-contact-test
(are [i e] (= (events/validate-contact (events/init-contact
{:user-public-key user-ukey
:input i}))
(events/init-contact e))
ens {:input ens
:id ens
:type :ens-name
:ens-name ens-stateofus-eth}
"" {:user-public-key user-ukey
:input ""
:type :empty
:state :empty}
ckey {:input ckey
:id ckey
:type :compressed-key
:ens-name nil}
" " {:user-public-key user-ukey
:input " "
:type :empty
:state :empty}
link-ckey {:input link-ckey
:id ckey
:type :compressed-key
:ens-name nil}
ukey {:user-public-key user-ukey
:input ukey
:id ukey
:type :public-key
:public-key ukey
:state :invalid
:msg :t/not-a-chatkey}
link-ens {:input link-ens
:id ens
:type :ens-name
:ens-name ens-stateofus-eth}))
ens {:user-public-key user-ukey
:input ens
:id ens
:type :ens
:ens ens-stateofus-eth
:state :resolve-ens}
(deftest search-empty-string-test
(is (= (core/set-new-identity {:db {:contacts/new-identity :foo}} "")
{:db {}})))
(str " " ens) {:user-public-key user-ukey
:input (str " " ens)
:id ens
:type :ens
:ens ens-stateofus-eth
:state :resolve-ens}
(deftest search-uncompressed-key-test
(is (= (core/set-new-identity {:db {}} ukey)
{:db {:contacts/new-identity
{:input ukey
:public-key ukey
:state :error
:error :uncompressed-key}}})))
ckey {:user-public-key user-ukey
:input ckey
:id ckey
:type :compressed-key
:state :decompress-key}
link-ckey {:user-public-key user-ukey
:input link-ckey
:id ckey
:type :compressed-key
:state :decompress-key}
link-ens {:user-public-key user-ukey
:input link-ens
:id ens
:type :ens
:ens ens-stateofus-eth
:state :resolve-ens}))
;;; event handler tests (no callbacks)
(def db
{:multiaccount {:public-key user-ukey}
:networks/current-network "mainnet_rpc"
:networks/networks {"mainnet_rpc"
{:id "mainnet_rpc"
:config {:NetworkId 1}}}})
(deftest set-new-identity-test
(with-redefs [events/dispatcher (fn [& args] args)]
(are [i edb] (= (events/set-new-identity {:db db} i nil) edb)
"" {:db db}
ukey {:db (assoc db
:contacts/new-identity
(events/init-contact
{:user-public-key user-ukey
:input ukey
:id ukey
:type :public-key
:public-key ukey
:state :invalid
:msg :t/not-a-chatkey}))}
ens {:db (assoc db
:contacts/new-identity
(events/init-contact
{:user-public-key user-ukey
:input ens
:id ens
:type :ens
:ens ens-stateofus-eth
:public-key nil ; not yet...
:state :resolve-ens}))
:contacts/resolve-public-key-from-ens
{:chain-id 1
:ens ens-stateofus-eth
:on-success [:contacts/set-new-identity-success ens]
:on-error [:contacts/set-new-identity-error ens]}}
;; compressed-key & add-self-as-contact
user-ckey {:db (assoc db
:contacts/new-identity
(events/init-contact
{:user-public-key user-ukey
:input user-ckey
:id user-ckey
:type :compressed-key
:public-key nil ; not yet...
:state :decompress-key}))
:contacts/decompress-public-key
{:compressed-key user-ckey
:on-success [:contacts/set-new-identity-success user-ckey]
:on-error [:contacts/set-new-identity-error user-ckey]}})))

View File

@ -32,7 +32,7 @@
{:style {:flex-direction :row
:justify-content :space-between}})
(def container-error
(def container-invalid
{:style {:flex-direction :row
:align-items :center
:margin-top 8}})
@ -64,18 +64,18 @@
colors/neutral-50
colors/neutral-40)}})
(def icon-error
(def icon-invalid
{:size 16
:color colors/danger-50})
(def text-error
(def text-invalid
{:size :paragraph-2
:align :left
:style {:margin-left 4
:color colors/danger-50}})
(defn text-input-container
[error?]
[invalid?]
{:style {:padding-top 1
:padding-left 12
:padding-right 7
@ -88,7 +88,7 @@
colors/neutral-95)
:border-width 1
:border-radius 12
:border-color (if error?
:border-color (if invalid?
colors/danger-50-opa-40
(colors/theme-colors
colors/neutral-20

View File

@ -4,6 +4,7 @@
[quo2.core :as quo]
[react-native.core :as rn]
[react-native.clipboard :as clipboard]
[reagent.core :as reagent]
[status-im2.common.resources :as resources]
[status-im.qr-scanner.core :as qr-scanner]
[status-im.utils.utils :as utils]
@ -44,60 +45,78 @@
(defn new-contact
[]
(let [{:keys [input public-key state error ens-name]} (rf/sub [:contacts/new-identity])
error? (and (= state :error)
(= error :uncompressed-key))]
[rn/keyboard-avoiding-view (style/container-kbd)
[rn/view style/container-image
[rn/image
{:source (resources/get-image :add-new-contact)
:style style/image}]
[quo/button
(merge (style/button-close)
{:on-press
(fn []
(rf/dispatch [:contacts/clear-new-identity])
(rf/dispatch [:navigate-back]))}) :i/close]]
[rn/view (style/container-outer)
[rn/view style/container-inner
[quo/text (style/text-title)
(i18n/label :t/add-a-contact)]
[quo/text (style/text-subtitle)
(i18n/label :t/find-your-friends)]
[quo/text (style/text-description)
(i18n/label :t/ens-or-chat-key)]
[rn/view style/container-text-input
[rn/view (style/text-input-container error?)
[rn/text-input
(merge (style/text-input)
{:default-value input
:placeholder (i18n/label :t/type-some-chat-key)
:on-change-text #(debounce/debounce-and-dispatch
[:contacts/set-new-identity %]
600)})]
(when (string/blank? input)
(let [clipboard (reagent/atom nil)
default-value (reagent/atom nil)]
(fn []
(clipboard/get-string #(reset! clipboard %))
(let [{:keys [input scanned public-key ens state msg]}
(rf/sub [:contacts/new-identity])
invalid? (= state :invalid)
show-paste-button? (and (not (string/blank? @clipboard))
(string/blank? @default-value)
(string/blank? input))]
[rn/keyboard-avoiding-view (style/container-kbd)
[rn/view style/container-image
[rn/image
{:source (resources/get-image :add-new-contact)
:style style/image}]
[quo/button
(merge (style/button-close)
{:on-press
(fn []
(reset! clipboard nil)
(reset! default-value nil)
(rf/dispatch [:contacts/clear-new-identity])
(rf/dispatch [:navigate-back]))}) :i/close]]
[rn/view (style/container-outer)
[rn/view style/container-inner
[quo/text (style/text-title)
(i18n/label :t/add-a-contact)]
[quo/text (style/text-subtitle)
(i18n/label :t/find-your-friends)]
[quo/text (style/text-description)
(i18n/label :t/ens-or-chat-key)]
[rn/view style/container-text-input
[rn/view (style/text-input-container invalid?)
[rn/text-input
(merge (style/text-input)
{:default-value (or scanned @default-value input)
:placeholder (i18n/label :t/type-some-chat-key)
:on-change-text (fn [v]
(reset! default-value v)
(debounce/debounce-and-dispatch
[:contacts/set-new-identity v nil]
600))})]
(when show-paste-button?
[quo/button
(merge style/button-paste
{:on-press
(fn []
(reset! default-value @clipboard)
(rf/dispatch
[:contacts/set-new-identity @clipboard nil]))})
(i18n/label :t/paste)])]
[quo/button
(merge style/button-qr
{:on-press #(rf/dispatch
[::qr-scanner/scan-code
{:handler :contacts/qr-code-scanned}])})
:i/scan]]
(when invalid?
[rn/view style/container-invalid
[quo/icon :i/alert style/icon-invalid]
[quo/text style/text-invalid
(i18n/label (or msg :t/invalid-ens-or-key))]])
(when (= state :valid)
[found-contact public-key])]
[rn/view
[quo/button
(merge style/button-paste
{:on-press (fn []
(clipboard/get-string #(rf/dispatch [:contacts/set-new-identity %])))})
(i18n/label :t/paste)])]
[quo/button
(merge style/button-qr
{:on-press #(rf/dispatch [::qr-scanner/scan-code
{:handler :contacts/qr-code-scanned}])})
:i/scan]]
(when error?
[rn/view style/container-error
[quo/icon :i/alert style/icon-error]
[quo/text style/text-error (i18n/label :t/not-a-chatkey)]])
(when (= state :valid)
[found-contact public-key])]
[rn/view
[quo/button
(merge (style/button-view-profile state)
{:on-press
(fn []
(rf/dispatch [:contacts/clear-new-identity])
(rf/dispatch [:navigate-back])
(rf/dispatch [:chat.ui/show-profile public-key ens-name]))})
(i18n/label :t/view-profile)]]]]))
(merge (style/button-view-profile state)
{:on-press
(fn []
(reset! clipboard nil)
(reset! default-value nil)
(rf/dispatch [:contacts/clear-new-identity])
(rf/dispatch [:navigate-back])
(rf/dispatch [:chat.ui/show-profile public-key ens]))})
(i18n/label :t/view-profile)]]]]))))

View File

@ -719,6 +719,7 @@
"invalid-pairing-password": "Invalid pairing password",
"invalid-range": "Invalid format, must be between {{min}} and {{max}}",
"invalid-username-or-key": "Invalid username or chat key",
"invalid-ens-or-key": "Invalid ENS or Chat key",
"join-me": "Hey join me on Status: {{url}}",
"join-a-community": "or join a community",
"join-open-community": "Join Community",