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

View File

@ -1,5 +1,6 @@
(ns status-im2.contexts.add-new-contact.events (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] [status-im.utils.types :as types]
[re-frame.core :as re-frame] [re-frame.core :as re-frame]
[status-im.ethereum.core :as ethereum] [status-im.ethereum.core :as ethereum]
@ -11,11 +12,120 @@
[status-im2.contexts.contacts.events :as data-store.contacts] [status-im2.contexts.contacts.events :as data-store.contacts]
[status-im.utils.utils :as utils])) [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 (re-frame/reg-fx
:contacts/decompress-public-key :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 (status/compressed-key->public-key
public-key compressed-key
(fn [resp] (fn [resp]
(let [{:keys [error]} (types/json->clj resp)] (let [{:keys [error]} (types/json->clj resp)]
(if error (if error
@ -23,74 +133,16 @@
(on-success (str "0x" (subs resp 5))))))))) (on-success (str "0x" (subs resp 5)))))))))
(re-frame/reg-fx (re-frame/reg-fx
:contacts/resolve-public-key-from-ens-name :contacts/resolve-public-key-from-ens
(fn [{:keys [chain-id ens-name on-success on-error]}] (fn [{:keys [chain-id ens on-success on-error]}]
(ens/pubkey chain-id ens-name on-success on-error))) (ens/pubkey chain-id ens 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))})))
(rf/defn build-contact (rf/defn build-contact
{:events [:contacts/build-contact]} {:events [:contacts/build-contact]}
[_ pubkey ens-name open-profile-modal?] [_ pubkey ens open-profile-modal?]
{:json-rpc/call [{:method "wakuext_buildContact" {:json-rpc/call [{:method "wakuext_buildContact"
:params [{:publicKey pubkey :params [{:publicKey pubkey
:ENSName ens-name}] :ENSName ens}]
:js-response true :js-response true
:on-success #(rf/dispatch [:contacts/contact-built :on-success #(rf/dispatch [:contacts/contact-built
pubkey pubkey
@ -106,24 +158,25 @@
(rf/defn set-new-identity-success (rf/defn set-new-identity-success
{:events [:contacts/set-new-identity-success]} {:events [:contacts/set-new-identity-success]}
[{:keys [db] :as cofx} input ens-name pubkey] [{:keys [db]} input pubkey]
(rf/merge cofx (let [contact (get-in db [:contacts/new-identity])]
{:db (assoc db (when (= (:input contact) input)
:contacts/new-identity (rf/merge {:db (assoc db
{:input input :contacts/new-identity
:public-key pubkey (->state (assoc contact :public-key pubkey)))}
:ens-name ens-name (build-contact pubkey (:ens contact) false)))))
:state :valid})}
(build-contact pubkey ens-name false)))
(rf/defn set-new-identity-error (rf/defn set-new-identity-error
{:events [:contacts/set-new-identity-error]} {:events [:contacts/set-new-identity-error]}
[{:keys [db]} error input] [{:keys [db]} input err]
{:db (assoc db (let [contact (get-in db [:contacts/new-identity])]
:contacts/new-identity (when (= (:input contact) input)
{:input input (let [state (cond
:state :error (or (string/includes? (:message err) "fallback failed")
:error :invalid})}) (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 (rf/defn clear-new-identity
{:events [:contacts/clear-new-identity :contacts/new-chat-focus]} {:events [:contacts/clear-new-identity :contacts/new-chat-focus]}
@ -132,7 +185,13 @@
(rf/defn qr-code-scanned (rf/defn qr-code-scanned
{:events [:contacts/qr-code-scanned]} {:events [:contacts/qr-code-scanned]}
[{:keys [db] :as cofx} input] [{:keys [db] :as cofx} scanned]
(rf/merge cofx (rf/merge cofx
(set-new-identity input) (set-new-identity scanned scanned)
(navigation/navigate-back))) (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 (ns status-im2.contexts.add-new-contact.events-test
(:require [cljs.test :refer-macros [deftest is are]] (:require [cljs.test :refer-macros [deftest are]]
[status-im2.contexts.add-new-contact.events :as core])) [status-im2.contexts.add-new-contact.events :as events]))
(def user-ukey
"0x04ca27ed9c7c4099d230c6d8853ad0cfaf084a019c543e9e433d3c04fac6de9147cf572b10e247cfe52f396b5aa10456b56dd1cf1d8a681e2b93993d44594b2e85")
(def user-ckey "zQ3shtFEo4PxpQiYGcNZZ8xhJmhD6WBXwnHPBueu5SRnvPXjk")
(def ukey (def ukey
"0x045596a7ff87da36860a84b0908191ce60a504afc94aac93c1abd774f182967ce694f1bf2d8773cd59f4dd0863e951f9b7f7351c5516291a0fceb73f8c392a0e88") "0x045596a7ff87da36860a84b0908191ce60a504afc94aac93c1abd774f182967ce694f1bf2d8773cd59f4dd0863e951f9b7f7351c5516291a0fceb73f8c392a0e88")
(def ckey "zQ3shWj4WaBdf2zYKCkXe6PHxDxNTzZyid1i75879Ue9cX9gA") (def ckey "zQ3shWj4WaBdf2zYKCkXe6PHxDxNTzZyid1i75879Ue9cX9gA")
@ -10,47 +13,118 @@
(def link-ckey (str "https://join.status.im/u/" ckey)) (def link-ckey (str "https://join.status.im/u/" ckey))
(def link-ens (str "https://join.status.im/u/" ens)) (def link-ens (str "https://join.status.im/u/" ens))
(deftest identify-type-test ;;; unit tests (no app-db involved)
(are [input expected] (= (core/identify-type input) expected)
"" {:input ""
:id nil
:type :empty
:ens-name nil}
ukey {:input ukey (deftest validate-contact-test
:id ukey (are [i e] (= (events/validate-contact (events/init-contact
:type :public-key {:user-public-key user-ukey
:ens-name nil} :input i}))
(events/init-contact e))
ens {:input ens "" {:user-public-key user-ukey
:id ens :input ""
:type :ens-name :type :empty
:ens-name ens-stateofus-eth} :state :empty}
ckey {:input ckey " " {:user-public-key user-ukey
:id ckey :input " "
:type :compressed-key :type :empty
:ens-name nil} :state :empty}
link-ckey {:input link-ckey ukey {:user-public-key user-ukey
:id ckey :input ukey
:type :compressed-key :id ukey
:ens-name nil} :type :public-key
:public-key ukey
:state :invalid
:msg :t/not-a-chatkey}
link-ens {:input link-ens ens {:user-public-key user-ukey
:id ens :input ens
:type :ens-name :id ens
:ens-name ens-stateofus-eth})) :type :ens
:ens ens-stateofus-eth
:state :resolve-ens}
(deftest search-empty-string-test (str " " ens) {:user-public-key user-ukey
(is (= (core/set-new-identity {:db {:contacts/new-identity :foo}} "") :input (str " " ens)
{:db {}}))) :id ens
:type :ens
:ens ens-stateofus-eth
:state :resolve-ens}
(deftest search-uncompressed-key-test ckey {:user-public-key user-ukey
(is (= (core/set-new-identity {:db {}} ukey) :input ckey
{:db {:contacts/new-identity :id ckey
{:input ukey :type :compressed-key
:public-key ukey :state :decompress-key}
:state :error
:error :uncompressed-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 {:style {:flex-direction :row
:justify-content :space-between}}) :justify-content :space-between}})
(def container-error (def container-invalid
{:style {:flex-direction :row {:style {:flex-direction :row
:align-items :center :align-items :center
:margin-top 8}}) :margin-top 8}})
@ -64,18 +64,18 @@
colors/neutral-50 colors/neutral-50
colors/neutral-40)}}) colors/neutral-40)}})
(def icon-error (def icon-invalid
{:size 16 {:size 16
:color colors/danger-50}) :color colors/danger-50})
(def text-error (def text-invalid
{:size :paragraph-2 {:size :paragraph-2
:align :left :align :left
:style {:margin-left 4 :style {:margin-left 4
:color colors/danger-50}}) :color colors/danger-50}})
(defn text-input-container (defn text-input-container
[error?] [invalid?]
{:style {:padding-top 1 {:style {:padding-top 1
:padding-left 12 :padding-left 12
:padding-right 7 :padding-right 7
@ -88,7 +88,7 @@
colors/neutral-95) colors/neutral-95)
:border-width 1 :border-width 1
:border-radius 12 :border-radius 12
:border-color (if error? :border-color (if invalid?
colors/danger-50-opa-40 colors/danger-50-opa-40
(colors/theme-colors (colors/theme-colors
colors/neutral-20 colors/neutral-20

View File

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

View File

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