General QR code scanner flow (#18677)

* QR scanner

* More options for QR

* Router fixes

* Updates for `on-qr-code-scanned`

* Update for communities

* Updates

* Scan profile path

* More fixes

* Last fixes for scanning

* Fixes

* Fixes

* Fixes

* Test fixes

* Fixes

* Updated Utils.m

* Test fix
This commit is contained in:
Alexander 2024-02-16 12:06:28 +01:00 committed by GitHub
parent 2e23dc7ad7
commit 49567596da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 244 additions and 75 deletions

View File

@ -120,9 +120,12 @@ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(checkAddressChecksum:(NSString *)address)
return StatusgoCheckAddressChecksum(address);
}
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(isAddress:(NSString *)address) {
return StatusgoIsAddress(address);
}
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(toChecksumAddress:(NSString *)address) {
return StatusgoToChecksumAddress(address);
}
@end

View File

@ -36,7 +36,7 @@
[(mailserver/process-next-messages-request)
(bottom-sheet/hide-bottom-sheet-old)
(wallet/restart-wallet-service nil)
(add-new-contact/set-new-identity-reconnected)]
#(add-new-contact/set-new-identity-reconnected %)]
logged-in?
[(mailserver/process-next-messages-request)

View File

@ -81,5 +81,9 @@
{:db (-> db
(assoc :contacts/identity identity)
(assoc :contacts/ens-name ens-name))
:dispatch [:contacts/build-contact identity ens-name true]}
:dispatch [:contacts/build-contact
{:pubkey identity
:ens ens-name
:success-fn (fn [_]
{:dispatch [:open-modal :profile]})}]}
{:dispatch [:navigate-to :my-profile]})))

View File

@ -55,7 +55,8 @@
:on-change (fn [text]
(re-frame/dispatch [:wallet-legacy/search-recipient-filter-changed text])
(re-frame/dispatch [:set-in [:contacts/new-identity :state] :searching])
(debounce/debounce-and-dispatch [:contacts/set-new-identity text] 300))}]]))
(debounce/debounce-and-dispatch [:contacts/set-new-identity {:input text}]
300))}]]))
(defn section
[_ _ _]

View File

@ -33,7 +33,7 @@
nil)]
[quo/top-nav
{:avatar-on-press #(rf/dispatch [:open-modal :settings])
:scan-on-press #(js/alert "to be implemented")
:scan-on-press #(rf/dispatch [:open-modal :shell-qr-reader])
:activity-center-on-press #(rf/dispatch [:activity-center/open])
:qr-code-on-press #(rf/dispatch [:open-modal :share-shell])
:container-style (merge style/top-nav-container container-style)

View File

@ -26,8 +26,18 @@
(def web2-domain "status.app")
(def user-path "u#")
(def user-with-data-path "u/")
(def community-path "c#")
(def community-with-data-path "c/")
(def channel-path "cc/")
(def web-urls (map #(str % web2-domain "/") web-prefixes))
(defn path-urls
[path]
(map #(str % path) web-urls))
(def handled-schemes (set (into uri-schemes web-urls)))
(def group-chat-extractor
@ -41,13 +51,13 @@
(def routes
[""
{handled-schemes {["c/" :community-data] :community
["cc/" :community-data] :community-chat
{handled-schemes {[community-with-data-path :community-data] :community
[channel-path :community-data] :community-chat
["p/" :chat-id] :private-chat
["cr/" :community-id] :community-requests
"g/" group-chat-extractor
["wallet/" :account] :wallet-account
["u/" :user-data] :user
[user-with-data-path :user-data] :user
"c" :community
"u" :user}
ethereum-scheme eip-extractor}])

View File

@ -1,13 +1,12 @@
(ns status-im.contexts.chat.home.add-new-contact.events
(:require
[clojure.string :as string]
[re-frame.core :as re-frame]
[status-im.common.validators :as validators]
[status-im.contexts.chat.contacts.events :as data-store.contacts]
status-im.contexts.chat.home.add-new-contact.effects
[status-im.navigation.events :as navigation]
[utils.ens.stateofus :as stateofus]
[utils.ethereum.chain :as chain]
[utils.re-frame :as rf]
[utils.string :as utils.string]))
(defn init-contact
@ -87,72 +86,64 @@
(def validate-contact (comp ->state ->type ->id))
(defn dispatcher [event input] (fn [arg] (rf/dispatch [event input arg])))
(declare build-contact)
(rf/defn set-new-identity
{:events [:contacts/set-new-identity]}
[{:keys [db]} input scanned]
(defn set-new-identity
[{:keys [db]} [{:keys [input build-success-fn failure-fn]}]]
(let [user-public-key (get-in db [:profile/profile :public-key])
{:keys [input id ens state]
:as contact} (-> {:user-public-key user-public-key
:input input
:scanned scanned}
:scanned input}
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)
:serialization/decompress-public-key
{:compressed-key id
:on-success
(dispatcher :contacts/set-new-identity-success input)
#(re-frame/dispatch [:contacts/set-new-identity-success
{:input input
:pubkey %
:build-success-fn build-success-fn}])
:on-error
(dispatcher :contacts/set-new-identity-error input)}}
#(re-frame/dispatch [:contacts/set-new-identity-error
{:input input
:pubkey %
:failure-fn failure-fn}])}}
:resolve-ens {:db (assoc db :contacts/new-identity contact)
:effects.contacts/resolve-public-key-from-ens
{:chain-id (chain/chain-id db)
:ens ens
:on-success
(dispatcher :contacts/set-new-identity-success input)
#(re-frame/dispatch [:contacts/set-new-identity-success
{:input input
:pubkey %
:build-success-fn build-success-fn}])
:on-error
(dispatcher :contacts/set-new-identity-error input)}})))
#(re-frame/dispatch [:contacts/set-new-identity-error
{:input input
:pubkey %
:failure-fn failure-fn}])}})))
(re-frame/reg-event-fx :contacts/set-new-identity set-new-identity)
(rf/defn build-contact
{:events [:contacts/build-contact]}
[_ pubkey ens open-profile-modal?]
{:json-rpc/call [{:method "wakuext_buildContact"
:params [{:publicKey pubkey
:ENSName ens}]
:js-response true
:on-success #(rf/dispatch [:contacts/contact-built
pubkey
open-profile-modal?
(data-store.contacts/<-rpc-js %)])}]})
(rf/defn contact-built
{:events [:contacts/contact-built]}
[{:keys [db]} pubkey open-profile-modal? contact]
(merge {:db (assoc-in db [:contacts/contacts pubkey] contact)}
(when open-profile-modal?
{:dispatch [:open-modal :profile]})))
(rf/defn set-new-identity-success
{:events [:contacts/set-new-identity-success]}
[{:keys [db]} input pubkey]
(defn- set-new-identity-success
[{:keys [db]} [{:keys [input pubkey build-success-fn]}]]
(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)))))
{:db (assoc db :contacts/new-identity (->state (assoc contact :public-key pubkey)))
:dispatch [:contacts/build-contact
{:pubkey pubkey
:ens (:ens contact)
:success-fn build-success-fn}]})))
(rf/defn set-new-identity-error
{:events [:contacts/set-new-identity-error]}
[{:keys [db]} input err]
(re-frame/reg-event-fx :contacts/set-new-identity-success set-new-identity-success)
(defn- set-new-identity-error
[{:keys [db]} [{:keys [input err failure-fn]}]]
(let [contact (get-in db [:contacts/new-identity])]
(when (= (:input contact) input)
(let [state (cond
@ -163,21 +154,41 @@
(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))}))))
(merge {:db (assoc db :contacts/new-identity (merge contact state))}
(when failure-fn
(failure-fn)))))))
(rf/defn clear-new-identity
{:events [:contacts/clear-new-identity :contacts/new-chat-focus]}
(re-frame/reg-event-fx :contacts/set-new-identity-error set-new-identity-error)
(defn- build-contact
[_ [{:keys [pubkey ens success-fn]}]]
{:json-rpc/call [{:method "wakuext_buildContact"
:params [{:publicKey pubkey
:ENSName ens}]
:js-response true
:on-success #(re-frame/dispatch [:contacts/build-contact-success
{:pubkey pubkey
:contact (data-store.contacts/<-rpc-js %)
:success-fn success-fn}])}]})
(re-frame/reg-event-fx :contacts/build-contact build-contact)
(defn- build-contact-success
[{:keys [db]} [{:keys [pubkey contact success-fn]}]]
(merge {:db (assoc-in db [:contacts/contacts pubkey] contact)}
(when success-fn
(success-fn contact))))
(re-frame/reg-event-fx :contacts/build-contact-success build-contact-success)
(defn- clear-new-identity
[{:keys [db]}]
{:db (dissoc db :contacts/new-identity)})
(rf/defn qr-code-scanned
{:events [:contacts/qr-code-scanned]}
[{:keys [db] :as cofx} scanned]
(rf/merge cofx
(set-new-identity scanned scanned)
(navigation/navigate-back)))
(re-frame/reg-event-fx :contacts/clear-new-identity clear-new-identity)
(rf/defn set-new-identity-reconnected
(defn set-new-identity-reconnected
[{:keys [db]}]
(let [input (get-in db [:contacts/new-identity :input])]
(rf/dispatch [:contacts/set-new-identity input])))
(re-frame/dispatch [:contacts/set-new-identity {:input input}])))

View File

@ -2,6 +2,7 @@
(:require
[cljs.test :refer-macros [deftest are]]
matcher-combinators.test
[re-frame.core :as re-frame]
[status-im.contexts.chat.home.add-new-contact.events :as events]))
(def user-ukey
@ -90,8 +91,8 @@
:config {:NetworkId 1}}}})
(deftest set-new-identity-test
(with-redefs [events/dispatcher (fn [& args] args)]
(are [i edb] (match? (events/set-new-identity {:db db} i nil) edb)
(with-redefs [re-frame/dispatch (fn [& args] args)]
(are [i edb] (match? (events/set-new-identity {:db db} [{:input i}]) edb)
"" {:db db}
@ -103,6 +104,7 @@
:id ukey
:type :public-key
:public-key ukey
:scanned ukey
:state :invalid
:msg :t/not-a-chatkey}))}
@ -115,6 +117,7 @@
:type :ens
:ens ens-stateofus-eth
:public-key nil ; not yet...
:scanned ens
:state :resolve-ens}))
:effects.contacts/resolve-public-key-from-ens
{:chain-id 1
@ -131,6 +134,7 @@
:id user-ckey
:type :compressed-key
:public-key nil ; not yet...
:scanned user-ckey
:state :decompress-key}))
:serialization/decompress-public-key
{:compressed-key user-ckey

View File

@ -13,7 +13,8 @@
(rn/dismiss-keyboard!))
[scan-qr-code/view
{:title (i18n/label :t/scan-qr)
:on-success-scan #(debounce/debounce-and-dispatch [:contacts/set-new-identity % %] 300)}]]))
:on-success-scan #(debounce/debounce-and-dispatch [:contacts/set-new-identity {:input %}]
300)}]]))
(defn view
[]

View File

@ -59,7 +59,8 @@
paste-on-input #(clipboard/get-string
(fn [clipboard-text]
(reset! input-value clipboard-text)
(rf/dispatch [:contacts/set-new-identity clipboard-text nil])))]
(rf/dispatch [:contacts/set-new-identity
{:input clipboard-text}])))]
(let [{:keys [scanned]} (rf/sub [:contacts/new-identity])
empty-input? (and (string/blank? @input-value)
(string/blank? scanned))]
@ -86,7 +87,7 @@
:value (or scanned @input-value)
:on-change-text (fn [new-text]
(reset! input-value new-text)
(as-> [:contacts/set-new-identity new-text nil] $
(as-> [:contacts/set-new-identity {:input new-text}] $
(if (string/blank? scanned)
(debounce/debounce-and-dispatch $ 600)
(rf/dispatch-sync $))))}]

View File

@ -0,0 +1,126 @@
(ns status-im.contexts.shell.qr-reader.view
(:require
[clojure.string :as string]
[react-native.core :as rn]
[react-native.hooks :as hooks]
[status-im.common.router :as router]
[status-im.common.scan-qr-code.view :as scan-qr-code]
[status-im.common.validators :as validators]
[status-im.contexts.communities.events]
[status-im.contexts.wallet.common.validation :as wallet-validation]
[utils.debounce :as debounce]
[utils.ethereum.eip.eip681 :as eip681]
[utils.i18n :as i18n]
[utils.url :as url]))
(def invalid-qr-toast
{:type :negative
:theme :dark
:text (i18n/label :t/invalid-qr)})
(defn- text-for-url-path?
[text path]
(some #(string/starts-with? text %) (router/path-urls path)))
(defn- extract-id
[scanned-text]
(let [index (string/index-of scanned-text "#")]
(subs scanned-text (inc index))))
(defn eth-address?
[scanned-text]
(wallet-validation/eth-address? scanned-text))
(defn eip681-address?
[scanned-text]
(-> scanned-text
eip681/parse-uri
:address
boolean))
(defn pairing-qr-code?
[_]
false)
(defn wallet-connect-code?
[scanned-text]
(string/starts-with? scanned-text "wc:"))
(defn url?
[scanned-text]
(url/url? scanned-text))
(defn load-and-show-profile
[address]
(debounce/debounce-and-dispatch
[:contacts/set-new-identity
{:input address
:build-success-fn (fn [{:keys [public-key ens-name]}]
{:dispatch-n [[:chat.ui/show-profile public-key ens-name]
[:contacts/clear-new-identity]]})
:failure-fn (fn []
{:dispatch [:toasts/upsert invalid-qr-toast]})}]
300))
(defn show-invalid-qr-toast
[]
(debounce/debounce-and-dispatch
[:toasts/upsert invalid-qr-toast]
300))
(defn on-qr-code-scanned
[scanned-text]
(cond
(text-for-url-path? scanned-text router/community-with-data-path)
;; TODO: https://github.com/status-im/status-mobile/issues/18743
nil
(text-for-url-path? scanned-text router/channel-path)
;; TODO: https://github.com/status-im/status-mobile/issues/18743
nil
(text-for-url-path? scanned-text router/user-with-data-path)
(let [address (extract-id scanned-text)]
(load-and-show-profile address))
(or (validators/valid-public-key? scanned-text)
(validators/valid-compressed-key? scanned-text))
(load-and-show-profile scanned-text)
(eth-address? scanned-text)
(debounce/debounce-and-dispatch [:navigate-to :wallet-accounts scanned-text] 300)
(eip681-address? scanned-text)
(do
(debounce/debounce-and-dispatch [:wallet-legacy/request-uri-parsed
(eip681/parse-uri scanned-text)]
300)
(debounce/debounce-and-dispatch [:navigate-change-tab :wallet-stack] 300))
(pairing-qr-code? scanned-text)
;; TODO: https://github.com/status-im/status-mobile/issues/18744
nil
(wallet-connect-code? scanned-text)
;; WalletConnect is not working yet, this flow should be updated once WalletConnect is ready
nil
(url? scanned-text)
(debounce/debounce-and-dispatch [:browser.ui/open-url scanned-text] 300)
:else
(show-invalid-qr-toast)))
(defn- f-internal-view
[]
(let [{:keys [keyboard-shown]} (hooks/use-keyboard)]
[:<>
(when keyboard-shown
(rn/dismiss-keyboard!))
[scan-qr-code/view
{:title (i18n/label :t/scan-qr)
:on-success-scan on-qr-code-scanned}]]))
(defn view
[]
[:f> f-internal-view])

View File

@ -29,7 +29,9 @@
:background :blur
:size 32
:accessibility-label :shell-scan-button
:on-press #(rf/dispatch [:navigate-back])}
:on-press (fn []
(rf/dispatch [:navigate-back])
(rf/dispatch [:open-modal :shell-qr-reader]))}
:i/scan]]
[quo/text
{:size :heading-1

View File

@ -43,6 +43,7 @@
[status-im.contexts.profile.settings.view :as settings]
[status-im.contexts.shell.activity-center.view :as activity-center]
[status-im.contexts.shell.jump-to.view :as shell]
[status-im.contexts.shell.qr-reader.view :as shell-qr-reader]
[status-im.contexts.shell.share.view :as share]
[status-im.contexts.syncing.find-sync-code.view :as find-sync-code]
[status-im.contexts.syncing.how-to-pair.view :as how-to-pair]
@ -91,6 +92,10 @@
{:name :shell-stack
:component shell/shell-stack}
{:name :shell-qr-reader
:options (assoc options/dark-screen :modalPresentationStyle :overCurrentContext)
:component shell-qr-reader/view}
{:name :chat
:options {:insets {:top? true}
:popGesture false}

View File

@ -78,7 +78,7 @@
(h/with-app-initialized
(h/with-account
;; search for contact using compressed key
(rf/dispatch [:contacts/set-new-identity compressed-key])
(rf/dispatch [:contacts/set-new-identity {:input compressed-key}])
(rf-test/wait-for
[:contacts/set-new-identity-success]
(let [new-identity @(rf/subscribe [:contacts/new-identity])]
@ -89,7 +89,7 @@
(rf-test/wait-for
[:contacts/build-contact]
(rf-test/wait-for
[:contacts/contact-built]
[:contacts/build-contact-success]
(let [contact @(rf/subscribe [:contacts/current-contact])]
(is (= primary-name (:primary-name contact))))
(h/logout)

View File

@ -1255,6 +1255,7 @@
"scan-or-enter-sync-code": "Scan or enter sync code",
"scan-qr": "Scan QR",
"scan-qr-code": "Scan QR code",
"invalid-qr": "Oops! This QR doesnt work with Status",
"search": "Search",
"search-discover-communities": "Search communities or categories",
"secret-keys-confirmation-text": "You will need them to continue to use your Keycard in case you ever lose your phone.",