diff --git a/src/status_im/components/camera.cljs b/src/status_im/components/camera.cljs index 497f9ae711..e07102e49a 100644 --- a/src/status_im/components/camera.cljs +++ b/src/status_im/components/camera.cljs @@ -27,3 +27,7 @@ (defn camera [props] (r/create-element default-camera (clj->js (merge {:inverted true} props)))) + +(defn get-qr-code-data [code] + (when (= "QR_CODE" (.-type code)) + (.-data code))) \ No newline at end of file diff --git a/src/status_im/i18n.cljs b/src/status_im/i18n.cljs index 13e1495d39..921419c26b 100644 --- a/src/status_im/i18n.cljs +++ b/src/status_im/i18n.cljs @@ -136,12 +136,18 @@ :delimiter delimiter :separator separator}))))) +(def default-option-value "") + +(defn label-options [options] + ;; i18n ignores nil value, leading to misleading messages + (into {} (for [[k v] options] [k (or v default-option-value)]))) + (defn label ([path] (label path {})) ([path options] (if (exists? rn-dependencies/i18n.t) (let [options (update options :amount label-number)] - (.t rn-dependencies/i18n (name path) (clj->js options))) + (.t rn-dependencies/i18n (name path) (clj->js (label-options options)))) (name path)))) (defn label-pluralize [count path & options] diff --git a/src/status_im/translations/en.cljs b/src/status_im/translations/en.cljs index 6902f9bbff..a77b68d7ef 100644 --- a/src/status_im/translations/en.cljs +++ b/src/status_im/translations/en.cljs @@ -413,7 +413,7 @@ :wallet-choose-recipient "Choose Recipient" :wallet-choose-from-contacts "Choose From Contacts" :wallet-address-from-clipboard "Use Address From Clipboard" - :wallet-invalid-address "Address is invalid" + :wallet-invalid-address "Invalid address: \n {{data}}" :wallet-browse-photos "Browse Photos" :validation-amount-invalid "Amount is not valid" :validation-amount-invalid-number "Amount is not a valid number" diff --git a/src/status_im/ui/screens/profile/qr_code/views.cljs b/src/status_im/ui/screens/profile/qr_code/views.cljs index 28249fd2a4..77ed4fccf2 100644 --- a/src/status_im/ui/screens/profile/qr_code/views.cljs +++ b/src/status_im/ui/screens/profile/qr_code/views.cljs @@ -6,7 +6,8 @@ [status-im.components.icons.vector-icons :as vi] [status-im.components.status-bar :refer [status-bar]] [status-im.i18n :refer [label]] - [status-im.ui.screens.profile.qr-code.styles :as styles]) + [status-im.ui.screens.profile.qr-code.styles :as styles] + [status-im.utils.eip.eip67 :as eip67]) (:require-macros [status-im.utils.views :refer [defview letsubs]])) (defview qr-code-view [] @@ -34,10 +35,7 @@ :height (.-height layout)}]))} (when (:width dimensions) [react/view {:style (styles/qr-code-container dimensions)} - [qr-code {:value (if amount? - (prn-str {:address (get contact qr-source) - :amount amount}) - (str "ethereum:" (get contact qr-source))) + [qr-code {:value (eip67/generate-uri (get contact qr-source) (when amount? {:value amount})) :size (- (min (:width dimensions) (:height dimensions)) 80)}]])] diff --git a/src/status_im/ui/screens/qr_scanner/events.cljs b/src/status_im/ui/screens/qr_scanner/events.cljs index 818cb68984..a4d993620a 100644 --- a/src/status_im/ui/screens/qr_scanner/events.cljs +++ b/src/status_im/ui/screens/qr_scanner/events.cljs @@ -1,10 +1,11 @@ (ns status-im.ui.screens.qr-scanner.events - (:require [re-frame.core :refer [after dispatch debug enrich]] + (:require [re-frame.core :as re-frame] [status-im.components.camera :as camera] [status-im.ui.screens.navigation :as nav] [status-im.utils.handlers :as u :refer [register-handler]] [status-im.utils.utils :as utils] - [status-im.i18n :as i18n])) + [status-im.i18n :as i18n] + [status-im.utils.eip.eip67 :as eip67])) (defmethod nav/preload-data! :qr-scanner [db [_ _ identifier]] @@ -15,16 +16,16 @@ (defn navigate-to-scanner [_ [_ identifier]] - (dispatch [:request-permissions - [:camera] - (fn [] - (camera/request-access - #(if % (dispatch [:navigate-to :qr-scanner identifier]) - (utils/show-popup (i18n/label :t/error) - (i18n/label :t/camera-access-error)))))])) + (re-frame/dispatch [:request-permissions + [:camera] + (fn [] + (camera/request-access + #(if % (re-frame/dispatch [:navigate-to :qr-scanner identifier]) + (utils/show-popup (i18n/label :t/error) + (i18n/label :t/camera-access-error)))))])) (register-handler :scan-qr-code - (after navigate-to-scanner) + (re-frame/after navigate-to-scanner) set-current-identifier) (register-handler :clear-qr-code @@ -34,7 +35,7 @@ (defn handle-qr-request [db [_ context data]] (when-let [handler (get-in db [:qr-codes context])] - (dispatch [handler context data]))) + (re-frame/dispatch [handler context (:address (eip67/parse-uri data))]))) (defn clear-qr-request [db [_ context]] (-> db @@ -44,7 +45,7 @@ (defn navigate-back! [{:keys [view-id]} _] (when (= :qr-scanner view-id) - (dispatch [:navigate-back]))) + (re-frame/dispatch [:navigate-back]))) (register-handler :set-qr-code (u/handlers-> diff --git a/src/status_im/ui/screens/qr_scanner/views.cljs b/src/status_im/ui/screens/qr_scanner/views.cljs index 1eb8737f7a..a0c96c594d 100644 --- a/src/status_im/ui/screens/qr_scanner/views.cljs +++ b/src/status_im/ui/screens/qr_scanner/views.cljs @@ -1,50 +1,42 @@ (ns status-im.ui.screens.qr-scanner.views (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [re-frame.core :refer [subscribe dispatch dispatch-sync]] - [status-im.components.react :refer [view - image]] - [status-im.components.camera :refer [camera]] - [status-im.components.styles :refer [icon-search - icon-back]] - [status-im.components.status-bar :refer [status-bar]] - [status-im.components.toolbar.view :refer [toolbar]] - [status-im.components.toolbar.actions :as act] - [status-im.components.toolbar.styles :refer [toolbar-background1]] - [status-im.ui.screens.qr-scanner.styles :as st] - [status-im.utils.types :refer [json->clj]] - [clojure.string :as str] - [reagent.core :as r])) + (:require [reagent.core :as reagent] + [re-frame.core :as re-frame] + [status-im.components.react :as react] + [status-im.components.camera :as camera] + [status-im.components.status-bar :as status-bar] + [status-im.components.toolbar.view :as toolbar] + [status-im.components.toolbar.actions :as action] + [status-im.components.toolbar.styles :as toolbar.styles] + [status-im.ui.screens.qr-scanner.styles :as styles])) (defview qr-scanner-toolbar [title hide-nav?] (letsubs [modal [:get :modal]] - [view - [status-bar] - [toolbar {:title title - :background-color toolbar-background1 - :hide-nav? hide-nav? - :nav-action (when modal - (act/back #(dispatch [:navigate-back])))}]])) + [react/view + [status-bar/status-bar] + [toolbar/toolbar {:title title + :background-color toolbar.styles/toolbar-background1 + :hide-nav? hide-nav? + :nav-action (when modal + (action/back #(re-frame/dispatch [:navigate-back])))}]])) (defview qr-scanner [] (letsubs [identifier [:get :current-qr-context] - camera-initialized? (r/atom false)] - [view st/barcode-scanner-container + camera-initialized? (reagent/atom false)] + [react/view styles/barcode-scanner-container [qr-scanner-toolbar (:toolbar-title identifier) (not @camera-initialized?)] - [camera {:onBarCodeRead (fn [code] - (let [data (-> (.-data code) - (str/replace #"ethereum:" ""))] - (dispatch [:set-qr-code identifier data]))) - ;:barCodeTypes [:qr] - :ref #(reset! camera-initialized? true) - :captureAudio false - :style st/barcode-scanner}] - [view st/rectangle-container - [view st/rectangle - [image {:source {:uri :corner_left_top} - :style st/corner-left-top}] - [image {:source {:uri :corner_right_top} - :style st/corner-right-top}] - [image {:source {:uri :corner_right_bottom} - :style st/corner-right-bottom}] - [image {:source {:uri :corner_left_bottom} - :style st/corner-left-bottom}]]]])) + [camera/camera {:onBarCodeRead #(re-frame/dispatch [:set-qr-code identifier (camera/get-qr-code-data %)]) + ;:barCodeTypes [:qr] + :ref #(reset! camera-initialized? true) + :captureAudio false + :style styles/barcode-scanner}] + [react/view styles/rectangle-container + [react/view styles/rectangle + [react/image {:source {:uri :corner_left_top} + :style styles/corner-left-top}] + [react/image {:source {:uri :corner_right_top} + :style styles/corner-right-top}] + [react/image {:source {:uri :corner_right_bottom} + :style styles/corner-right-bottom}] + [react/image {:source {:uri :corner_left_bottom} + :style styles/corner-left-bottom}]]]])) diff --git a/src/status_im/ui/screens/wallet/choose_recipient/events.cljs b/src/status_im/ui/screens/wallet/choose_recipient/events.cljs index c79a43a0fb..97a8866dca 100644 --- a/src/status_im/ui/screens/wallet/choose_recipient/events.cljs +++ b/src/status_im/ui/screens/wallet/choose_recipient/events.cljs @@ -1,5 +1,6 @@ (ns status-im.ui.screens.wallet.choose-recipient.events (:require [status-im.i18n :as i18n] + [status-im.utils.eip.eip67 :as eip67] [status-im.utils.handlers :as handlers])) (handlers/register-handler-db @@ -14,13 +15,14 @@ (handlers/register-handler-fx :choose-recipient - (fn [{{:keys [web3] :as db} :db} [_ address name]] + (fn [{{:keys [web3] :as db} :db} [_ data name]] (let [{:keys [view-id]} db + address (:address (eip67/parse-uri data)) valid-address? (.isAddress web3 address)] (cond-> {:db db} (= :choose-recipient view-id) (assoc :dispatch [:navigate-back]) valid-address? (update :db #(choose-address-and-name % address name)) - (not valid-address?) (assoc :show-error (i18n/label :t/wallet-invalid-address)))))) + (not valid-address?) (assoc :show-error (i18n/label :t/wallet-invalid-address {:data data})))))) (handlers/register-handler-fx :wallet-open-send-transaction diff --git a/src/status_im/ui/screens/wallet/choose_recipient/views.cljs b/src/status_im/ui/screens/wallet/choose_recipient/views.cljs index 1b406d5956..dddbaec6d2 100644 --- a/src/status_im/ui/screens/wallet/choose_recipient/views.cljs +++ b/src/status_im/ui/screens/wallet/choose_recipient/views.cljs @@ -1,21 +1,16 @@ (ns status-im.ui.screens.wallet.choose-recipient.views (:require-macros [status-im.utils.views :refer [defview letsubs]]) (:require [re-frame.core :as re-frame] - [status-im.utils.utils :as utils] + [status-im.components.camera :as camera] + [status-im.components.icons.vector-icons :as vector-icons] + [status-im.components.react :as react] + [status-im.components.status-bar :as status-bar] [status-im.components.toolbar-new.view :as toolbar] [status-im.components.toolbar-new.actions :as act] [status-im.i18n :as i18n] - [status-im.ui.screens.wallet.styles :as wallet.styles] - [status-im.components.react :as react] - [status-im.components.icons.vector-icons :as vector-icons] [status-im.ui.screens.wallet.choose-recipient.styles :as styles] [status-im.utils.platform :as platform] - [status-im.components.status-bar :as status-bar] - [status-im.components.camera :as camera] - [clojure.string :as string])) - -(defn- show-not-implemented! [] - (utils/show-popup "TODO" "Not implemented yet!")) + [status-im.ui.screens.wallet.styles :as wallet.styles])) (defn choose-from-contacts [] (re-frame/dispatch [:navigate-to-modal @@ -88,15 +83,10 @@ {:width (.-width layout) :height (.-height layout)}]))} (when (or platform/android? - camera-permitted?) - [camera/camera {:style styles/preview - :aspect :fill - :captureAudio false - :torchMode (camera/set-torch camera-flashlight) - :onBarCodeRead (fn [code] - (let [data (-> code - .-data - (string/replace #"ethereum:" ""))] - (re-frame/dispatch [:choose-recipient data nil])))}]) + camera-permitted?)[camera/camera {:style styles/preview + :aspect :fill + :captureAudio false + :torchMode (camera/set-torch camera-flashlight) + :onBarCodeRead #(re-frame/dispatch [:choose-recipient (camera/get-qr-code-data %) nil])}]) [viewfinder camera-dimensions]] [recipient-buttons]])) diff --git a/src/status_im/ui/screens/wallet/request/views.cljs b/src/status_im/ui/screens/wallet/request/views.cljs index a37e7b0d61..1e22b436a8 100644 --- a/src/status_im/ui/screens/wallet/request/views.cljs +++ b/src/status_im/ui/screens/wallet/request/views.cljs @@ -13,6 +13,7 @@ [status-im.ui.screens.wallet.request.styles :as styles] [status-im.components.styles :as components.styles] [status-im.i18n :as i18n] + [status-im.utils.eip.eip67 :as eip67] [status-im.utils.platform :as platform])) (defn toolbar-view [] @@ -30,8 +31,7 @@ (views/defview qr-code [] (views/letsubs [account [:get-current-account]] [components.qr-code/qr-code - {:value (.stringify js/JSON (clj->js {:address (:address account) - :amount 0})) + {:value (eip67/generate-uri (:address account)) :size 256}])) (views/defview request-transaction [] diff --git a/src/status_im/ui/screens/wallet/send/events.cljs b/src/status_im/ui/screens/wallet/send/events.cljs index cc0913ff66..01c84e0a40 100644 --- a/src/status_im/ui/screens/wallet/send/events.cljs +++ b/src/status_im/ui/screens/wallet/send/events.cljs @@ -1,13 +1,13 @@ (ns status-im.ui.screens.wallet.send.events - (:require [re-frame.core :as re-frame] + (:require [clojure.string :as string] + [re-frame.core :as re-frame] + [status-im.i18n :as i18n] + [status-im.native-module.core :as status] [status-im.utils.handlers :as handlers] [status-im.ui.screens.wallet.db :as wallet.db] - [status-im.native-module.core :as status] [status-im.utils.types :as types] - [clojure.string :as string] [status-im.utils.money :as money] - [status-im.utils.utils :as utils] - [status-im.i18n :as i18n])) + [status-im.utils.utils :as utils])) ;;;; FX diff --git a/src/status_im/utils/eip/eip67.cljs b/src/status_im/utils/eip/eip67.cljs new file mode 100644 index 0000000000..3890380e4a --- /dev/null +++ b/src/status_im/utils/eip/eip67.cljs @@ -0,0 +1,40 @@ +(ns status-im.utils.eip.eip67 + "Utility function related to [EIP67](https://github.com/ethereum/EIPs/issues/67)" + (:require [clojure.string :as string])) + +(def scheme "ethereum") +(def scheme-separator ":") +(def parameters-separator "?") +(def parameter-separator "&") +(def key-value-separator "=") + +(def key-value-format (str "([^" parameter-separator key-value-separator "]+)")) +(def parameters-pattern (re-pattern (str key-value-format key-value-separator key-value-format))) + +(defn- parse-parameters [s] + (when s + (into {} (for [[_ k v] (re-seq parameters-pattern s)] + [(keyword k) v])))) + +(defn parse-uri + "Parse a EIP 67 URI as a map of keyword / strings. Parsed map will contain at least the key `address`. + Invalid URI will be parsed as `nil`." + [s] + (when (and s (string/starts-with? s scheme)) + (let [[address parameters] (string/split (string/replace s (str scheme scheme-separator) "") parameters-separator)] + (when-not (zero? (count address)) + (merge + {:address address} + (parse-parameters parameters)))))) + +(defn- generate-parameter-string [m] + (string/join parameter-separator (for [[k v] m] + (str (name k) key-value-separator v)))) + +(defn generate-uri + "Generate a EIP 67 URI based on `address` and an optional map of extra properties. + No validation of address format is performed." + ([address] (generate-uri address nil)) + ([address m] + (when address + (str scheme scheme-separator address (when m (str parameters-separator (generate-parameter-string m))))))) \ No newline at end of file diff --git a/test/cljs/status_im/test/handlers.cljs b/test/cljs/status_im/test/handlers.cljs deleted file mode 100644 index 304dcfd136..0000000000 --- a/test/cljs/status_im/test/handlers.cljs +++ /dev/null @@ -1,3 +0,0 @@ -(ns status-im.test.handlers - (:require [cljs.test :refer-macros [deftest is]] - [status-im.ui.screens.events :as events])) \ No newline at end of file diff --git a/test/cljs/status_im/test/i18n.cljs b/test/cljs/status_im/test/i18n.cljs new file mode 100644 index 0000000000..42be2f7be3 --- /dev/null +++ b/test/cljs/status_im/test/i18n.cljs @@ -0,0 +1,6 @@ +(ns status-im.test.i18n + (:require [cljs.test :refer-macros [deftest is]] + [status-im.i18n :as i18n])) + +(deftest label-options + (is (not (nil? (:key (i18n/label-options {:key nil})))))) diff --git a/test/cljs/status_im/test/runner.cljs b/test/cljs/status_im/test/runner.cljs index 480eed40de..6cd901f23c 100644 --- a/test/cljs/status_im/test/runner.cljs +++ b/test/cljs/status_im/test/runner.cljs @@ -8,7 +8,7 @@ [status-im.test.profile.events] [status-im.test.chat.models.input] [status-im.test.components.main-tabs] - [status-im.test.handlers] + [status-im.test.i18n] [status-im.test.utils.utils] [status-im.test.utils.money] [status-im.test.utils.clocks] @@ -34,7 +34,7 @@ 'status-im.test.wallet.transactions.subs 'status-im.test.chat.models.input 'status-im.test.components.main-tabs - 'status-im.test.handlers + 'status-im.test.i18n 'status-im.test.utils.utils 'status-im.test.utils.money 'status-im.test.utils.clocks diff --git a/test/cljs/status_im/test/utils/eip/eip67.cljs b/test/cljs/status_im/test/utils/eip/eip67.cljs new file mode 100644 index 0000000000..de64b74d80 --- /dev/null +++ b/test/cljs/status_im/test/utils/eip/eip67.cljs @@ -0,0 +1,18 @@ +(ns status-im.test.utils.eip.eip67 + (:require [cljs.test :refer-macros [deftest is testing]] + [status-im.utils.eip.eip67 :as eip67])) + +(deftest parse-uri + (is (= nil (eip67/parse-uri nil))) + (is (= nil (eip67/parse-uri "random"))) + (is (= nil (eip67/parse-uri "ethereum:"))) + (is (= nil (eip67/parse-uri "ethereum:?value=1"))) + (is (= nil (eip67/parse-uri "bitcoin:0x1234"))) + (is (= {:address "0x1234"} (eip67/parse-uri "ethereum:0x1234"))) + (is (= {:address "0x1234" :to "0x5678" :value "1"} (eip67/parse-uri "ethereum:0x1234?to=0x5678&value=1")))) + +(deftest generate-uri + (is (= nil (eip67/generate-uri nil))) + (is (= "ethereum:0x1234" (eip67/generate-uri "0x1234"))) + (is (= "ethereum:0x1234?to=0x5678" (eip67/generate-uri "0x1234" {:to "0x5678"}))) + (is (= "ethereum:0x1234?to=0x5678&value=1" (eip67/generate-uri "0x1234" {:to "0x5678" :value 1})))) \ No newline at end of file