From 49567596dac47f7b54e5c8113ce43d74e68d18cf Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 16 Feb 2024 12:06:28 +0100 Subject: [PATCH] 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 --- .../react-native-status/ios/RCTStatus/Utils.m | 5 +- .../status_im/mobile_sync_settings/core.cljs | 2 +- src/legacy/status_im/profile/core.cljs | 6 +- .../ui/screens/wallet/recipient/views.cljs | 3 +- src/status_im/common/home/top_nav/view.cljs | 2 +- src/status_im/common/router.cljs | 28 ++-- .../chat/home/add_new_contact/events.cljs | 117 ++++++++-------- .../home/add_new_contact/events_test.cljs | 8 +- .../scan/scan_profile_qr_page.cljs | 3 +- .../chat/home/add_new_contact/views.cljs | 5 +- .../contexts/shell/qr_reader/view.cljs | 126 ++++++++++++++++++ src/status_im/contexts/shell/share/view.cljs | 4 +- src/status_im/navigation/screens.cljs | 5 + src/tests/integration_test/chat_test.cljs | 4 +- translations/en.json | 1 + 15 files changed, 244 insertions(+), 75 deletions(-) create mode 100644 src/status_im/contexts/shell/qr_reader/view.cljs diff --git a/modules/react-native-status/ios/RCTStatus/Utils.m b/modules/react-native-status/ios/RCTStatus/Utils.m index 2d2be69a51..e9b0149fc3 100644 --- a/modules/react-native-status/ios/RCTStatus/Utils.m +++ b/modules/react-native-status/ios/RCTStatus/Utils.m @@ -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 diff --git a/src/legacy/status_im/mobile_sync_settings/core.cljs b/src/legacy/status_im/mobile_sync_settings/core.cljs index 0fd21df4a1..46b544ddd9 100644 --- a/src/legacy/status_im/mobile_sync_settings/core.cljs +++ b/src/legacy/status_im/mobile_sync_settings/core.cljs @@ -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) diff --git a/src/legacy/status_im/profile/core.cljs b/src/legacy/status_im/profile/core.cljs index af36e50edc..b897f97b0a 100644 --- a/src/legacy/status_im/profile/core.cljs +++ b/src/legacy/status_im/profile/core.cljs @@ -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]}))) diff --git a/src/legacy/status_im/ui/screens/wallet/recipient/views.cljs b/src/legacy/status_im/ui/screens/wallet/recipient/views.cljs index 6fc8828a98..8b223b67d5 100644 --- a/src/legacy/status_im/ui/screens/wallet/recipient/views.cljs +++ b/src/legacy/status_im/ui/screens/wallet/recipient/views.cljs @@ -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 [_ _ _] diff --git a/src/status_im/common/home/top_nav/view.cljs b/src/status_im/common/home/top_nav/view.cljs index 126700e684..e1625b7d6e 100644 --- a/src/status_im/common/home/top_nav/view.cljs +++ b/src/status_im/common/home/top_nav/view.cljs @@ -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) diff --git a/src/status_im/common/router.cljs b/src/status_im/common/router.cljs index df497c8ac0..170aa20413 100644 --- a/src/status_im/common/router.cljs +++ b/src/status_im/common/router.cljs @@ -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,15 +51,15 @@ (def routes ["" - {handled-schemes {["c/" :community-data] :community - ["cc/" :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 - "c" :community - "u" :user} + {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 + [user-with-data-path :user-data] :user + "c" :community + "u" :user} ethereum-scheme eip-extractor}]) (defn parse-query-params diff --git a/src/status_im/contexts/chat/home/add_new_contact/events.cljs b/src/status_im/contexts/chat/home/add_new_contact/events.cljs index e2be045fce..dca1add368 100644 --- a/src/status_im/contexts/chat/home/add_new_contact/events.cljs +++ b/src/status_im/contexts/chat/home/add_new_contact/events.cljs @@ -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}]))) + diff --git a/src/status_im/contexts/chat/home/add_new_contact/events_test.cljs b/src/status_im/contexts/chat/home/add_new_contact/events_test.cljs index 913fd00ef6..cd68a70945 100644 --- a/src/status_im/contexts/chat/home/add_new_contact/events_test.cljs +++ b/src/status_im/contexts/chat/home/add_new_contact/events_test.cljs @@ -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 diff --git a/src/status_im/contexts/chat/home/add_new_contact/scan/scan_profile_qr_page.cljs b/src/status_im/contexts/chat/home/add_new_contact/scan/scan_profile_qr_page.cljs index 8241f8c0e1..a8a7ed90ac 100644 --- a/src/status_im/contexts/chat/home/add_new_contact/scan/scan_profile_qr_page.cljs +++ b/src/status_im/contexts/chat/home/add_new_contact/scan/scan_profile_qr_page.cljs @@ -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 [] diff --git a/src/status_im/contexts/chat/home/add_new_contact/views.cljs b/src/status_im/contexts/chat/home/add_new_contact/views.cljs index 9e23b2c311..8919c7ff99 100644 --- a/src/status_im/contexts/chat/home/add_new_contact/views.cljs +++ b/src/status_im/contexts/chat/home/add_new_contact/views.cljs @@ -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 $))))}] diff --git a/src/status_im/contexts/shell/qr_reader/view.cljs b/src/status_im/contexts/shell/qr_reader/view.cljs new file mode 100644 index 0000000000..19a4835d8c --- /dev/null +++ b/src/status_im/contexts/shell/qr_reader/view.cljs @@ -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]) diff --git a/src/status_im/contexts/shell/share/view.cljs b/src/status_im/contexts/shell/share/view.cljs index da5c72bd75..8c7f23232c 100644 --- a/src/status_im/contexts/shell/share/view.cljs +++ b/src/status_im/contexts/shell/share/view.cljs @@ -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 diff --git a/src/status_im/navigation/screens.cljs b/src/status_im/navigation/screens.cljs index b1cb11514c..95c1f0ccb1 100644 --- a/src/status_im/navigation/screens.cljs +++ b/src/status_im/navigation/screens.cljs @@ -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} diff --git a/src/tests/integration_test/chat_test.cljs b/src/tests/integration_test/chat_test.cljs index 2728b24a5c..ce3e722f9b 100644 --- a/src/tests/integration_test/chat_test.cljs +++ b/src/tests/integration_test/chat_test.cljs @@ -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) diff --git a/translations/en.json b/translations/en.json index d5453332a7..6c804bc358 100644 --- a/translations/en.json +++ b/translations/en.json @@ -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 doesn’t 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.",