diff --git a/components/src/status_im/ui/components/react.cljs b/components/src/status_im/ui/components/react.cljs index a667b683c3..a1a42ad98c 100644 --- a/components/src/status_im/ui/components/react.cljs +++ b/components/src/status_im/ui/components/react.cljs @@ -88,9 +88,9 @@ (dissoc :font) (assoc style-key (merge style font))))) -(defn transform-to-uppercase [{:keys [uppercase? force-uppercase?] :as opts} ts] +(defn transform-to-uppercase [{:keys [uppercase? force-uppercase?]} ts] (if (or force-uppercase? (and uppercase? platform/android?)) - (vec (map string/upper-case ts)) + (vec (map #(when % (string/upper-case %)) ts)) ts)) (defn text diff --git a/src/status_im/chat/commands/core.cljs b/src/status_im/chat/commands/core.cljs index 4a476e490e..40c9188568 100644 --- a/src/status_im/chat/commands/core.cljs +++ b/src/status_im/chat/commands/core.cljs @@ -130,24 +130,27 @@ (def command-hook "Hook for extensions" {:properties - {:scope #{:personal-chats :public-chats} + {:description? :string + :scope #{:personal-chats :public-chats} :short-preview :view :preview :view + :on-send? :event + :on-receive? :event :parameters [{:id :keyword :type {:one-of #{:text :phone :password :number}} :placeholder :string :suggestions :view}]} :hook (reify hooks/Hook - (hook-in [_ id {:keys [description scope parameters preview short-preview]} cofx] + (hook-in [_ id {:keys [description scope parameters preview short-preview on-send on-receive]} cofx] (let [new-command (reify protocol/Command (id [_] (name id)) (scope [_] scope) (description [_] description) (parameters [_] parameters) (validate [_ _ _]) - (on-send [_ _ _]) - (on-receive [_ _ _]) + (on-send [_ _ _] {:dispatch on-send}) + (on-receive [_ _ _] {:dispatch on-receive}) (short-preview [_ props] (short-preview props)) (preview [_ props] (preview props)))] (load-commands cofx [new-command]))) diff --git a/src/status_im/chat/commands/impl/transactions.cljs b/src/status_im/chat/commands/impl/transactions.cljs index 7d39d3f0d2..0aa4161d32 100644 --- a/src/status_im/chat/commands/impl/transactions.cljs +++ b/src/status_im/chat/commands/impl/transactions.cljs @@ -131,7 +131,7 @@ (defn choose-nft-token-suggestion [selected-event-creator] [choose-nft-token selected-event-creator]) -(defview nft-token [{:keys [name image_url] :as token}] +(defview nft-token [{{:keys [name image_url]} :token}] [react/view {:flex-direction :column :align-items :center} [svgimage/svgimage {:style {:width 100 @@ -200,6 +200,9 @@ tx-exists? :status-pending :else :status-tx-not-found))]]]])) +(defn transaction-status [{:keys [tx-hash outgoing]}] + [send-status tx-hash outgoing]) + (defview send-preview [{:keys [content timestamp-str outgoing group-chat]}] (letsubs [network [:network-name]] diff --git a/src/status_im/chat/commands/protocol.cljs b/src/status_im/chat/commands/protocol.cljs index 83ef65bae3..50c5ba9966 100644 --- a/src/status_im/chat/commands/protocol.cljs +++ b/src/status_im/chat/commands/protocol.cljs @@ -33,7 +33,7 @@ "Function which can provide any extra effects to be produced in addition to normal message effects which happen whenever message is sent") (on-receive [this command-message cofx] - "Function which can provide any extre effects to be produced in addition to + "Function which can provide any extra effects to be produced in addition to normal message effects which happen when particular command message is received") (short-preview [this command-message] "Function rendering the short-preview of the command message, used when diff --git a/src/status_im/chat/subs.cljs b/src/status_im/chat/subs.cljs index a22b6a1eb5..960b371352 100644 --- a/src/status_im/chat/subs.cljs +++ b/src/status_im/chat/subs.cljs @@ -286,7 +286,7 @@ :chat-parameter-box :<- [:get-current-chat] :<- [:selected-chat-command] - (fn [[current-chat {:keys [current-param-position params]}]] + (fn [[_ {:keys [current-param-position params]}]] (when (and params current-param-position) (get-in params [current-param-position :suggestions])))) diff --git a/src/status_im/extensions/core.cljs b/src/status_im/extensions/core.cljs index 2d85e1099e..dece32f295 100644 --- a/src/status_im/extensions/core.cljs +++ b/src/status_im/extensions/core.cljs @@ -1,36 +1,162 @@ (ns status-im.extensions.core (:require [clojure.string :as string] + [re-frame.core :as re-frame] [pluto.reader :as reader] - [pluto.registry :as registry] [pluto.storages :as storages] [status-im.chat.commands.core :as commands] [status-im.chat.commands.impl.transactions :as transactions] [status-im.ui.components.react :as react] + [status-im.ui.components.button.view :as button] + [status-im.utils.handlers :as handlers] [status-im.ui.screens.navigation :as navigation] [status-im.utils.fx :as fx])) -(def components - {'view {:value react/view} - 'text {:value react/text} - 'nft-token {:value transactions/nft-token} - 'send-status {:value transactions/send-status} - 'asset-selector {:value transactions/choose-nft-asset-suggestion} - 'token-selector {:value transactions/choose-nft-token-suggestion}}) +(re-frame/reg-fx + ::alert + (fn [value] (js/alert value))) -(def app-hooks #{commands/command-hook}) +(re-frame/reg-event-fx + :alert + (fn [_ [_ {:keys [value]}]] + {::alert value})) + +(re-frame/reg-fx + ::log + (fn [value] (js/console.log value))) + +(re-frame/reg-event-fx + :log + (fn [_ [_ {:keys [value]}]] + {::log value})) + +(re-frame/reg-sub + :store/get + (fn [db [_ {:keys [key]}]] + (get-in db [:extensions-store :collectible key]))) + +(handlers/register-handler-fx + :store/put + (fn [{:keys [db]} [_ {:keys [key value]}]] + {:db (assoc-in db [:extensions-store :collectible key] value)})) + +(defn- append [acc k v] + (let [o (get acc k)] + (assoc acc k (conj (if (vector? o) o (vector o)) v)))) + +(handlers/register-handler-fx + :store/append + (fn [{:keys [db]} [_ {:keys [key value]}]] + {:db (update-in db [:extensions-store :collectible] append key value)})) + +(handlers/register-handler-fx + :store/clear + (fn [{:keys [db]} [_ {:keys [key]}]] + {:db (update-in db [:extensions-store :collectible] dissoc key)})) + +(re-frame/reg-event-fx + :http/get + (fn [_ [_ {:keys [url on-success on-failure timeout]}]] + {:http-get (merge {:url url + :success-event-creator (fn [o] (into on-success (vector o)))} + (when on-failure + {:failure-event-creator (fn [o] (into on-failure (vector o)))}) + (when timeout + {:timeout-ms timeout}))})) + +(defn button [{:keys [on-click]} label] + [button/secondary-button {:on-press #(re-frame/dispatch on-click)} label]) + +(defn input [{:keys [on-change placeholder]}] + [react/text-input {:on-change-text #(re-frame/dispatch on-change) :placeholder placeholder}]) (def capacities - (reduce (fn [capacities hook] - (assoc-in capacities [:hooks :commands] hook)) - {:components components - :queries {'get-collectible-token {:value :get-collectible-token}} - :events {}} - app-hooks)) + {:components {'view {:value react/view} + 'text {:value react/text} + 'input {:value input :properties {:on-change :event :placeholder :string}} + 'button {:value button :properties {:on-click :event}} + 'nft-token-viewer {:value transactions/nft-token :properties {:token :string}} + 'transaction-status {:value transactions/transaction-status :properties {:outgoing :string :tx-hash :string}} + 'asset-selector {:value transactions/choose-nft-asset-suggestion} + 'token-selector {:value transactions/choose-nft-token-suggestion}} + :queries {'store/get {:value :store/get :arguments {:key :string}} + 'get-collectible-token {:value :get-collectible-token :arguments {:token :string :symbol :string}}} + :events {'alert + {:permissions [:read] + :value :alert + :arguments {:value :string}} + 'log + {:permissions [:read] + :value :log + :arguments {:value :string}} + 'store/put + {:permissions [:read] + :value :store/put + :arguments {:key :string :value :string}} + 'store/append + {:permissions [:read] + :value :store/append + :arguments {:key :string :value :string}} + 'store/clear + {:permissions [:read] + :value :store/put + :arguments {:key :string}} + 'http/get + {:permissions [:read] + :value :http/get + :arguments {:url :string + :timeout? :string + :on-success :event + :on-failure? :event}} + 'browser/open {:value :browser/open :arguments {:url :string}} + 'chat/open {:value :chat/open :arguments {:url :string}} + 'ethereum/sign + {:arguments + {:account :string + :message :string + :on-result :event}} + 'ethereum/send-raw-transaction + {:arguments {:data :string}} + 'ethereum/send-transaction + {:arguments + {:from :string + :to :string + :gas? :string + :gas-price? :string + :value? :string + :data? :string + :nonce? :string}} + 'ethereum/new-contract + {:arguments + {:from :string + :gas? :string + :gas-price? :string + :value? :string + :data? :string + :nonce? :string}} + 'ethereum/call + {:arguments + {:from? :string + :to :string + :gas? :string + :gas-price? :string + :value? :string + :data? :string + :block :string}} + 'ethereum/logs + {:arguments + {:from? :string + :to :string + :address :string + :topics :string + :blockhash :string}}} + :hooks {:commands commands/command-hook}}) -(defn read-extension [o] - (-> o :value first :content reader/read)) +(defn read-extension [{:keys [value]}] + (when (seq value) + (let [{:keys [content]} (first value)] + (reader/read content)))) -(defn parse [{:keys [data] :as m}] +(defn parse [{:keys [data]}] (try (let [{:keys [errors] :as extension-data} (reader/parse {:capacities capacities} data)] (when errors @@ -38,9 +164,16 @@ extension-data) (catch :default e (println "EXC" e)))) +(def uri-prefix "https://get.status.im/extension/") + +(defn valid-uri? [s] + (boolean + (when s + (re-matches (re-pattern (str "^" uri-prefix "\\w+@\\w+")) (string/trim s))))) + (defn url->uri [s] (when s - (string/replace s "https://get.status.im/extension/" ""))) + (string/replace s uri-prefix ""))) (defn load-from [url f] (when-let [uri (url->uri url)] diff --git a/src/status_im/ui/components/text.cljs b/src/status_im/ui/components/text.cljs deleted file mode 100644 index 354e30065f..0000000000 --- a/src/status_im/ui/components/text.cljs +++ /dev/null @@ -1,3 +0,0 @@ -(ns status-im.ui.components.text - (:require [status-im.ui.components.react :as react] - [status-im.utils.platform :as platform])) diff --git a/src/status_im/ui/screens/db.cljs b/src/status_im/ui/screens/db.cljs index c8e01d3a2f..911e3297ae 100644 --- a/src/status_im/ui/screens/db.cljs +++ b/src/status_im/ui/screens/db.cljs @@ -160,6 +160,7 @@ (spec/def ::extension-url (spec/nilable string?)) (spec/def ::staged-extension (spec/nilable any?)) +(spec/def ::extensions-store (spec/nilable any?)) ;;;;NODE @@ -235,6 +236,7 @@ :desktop/desktop :dimensions/window :dapps/permissions] + :opt-un [::current-public-key ::modal @@ -308,4 +310,5 @@ ::collectibles ::extension-url ::staged-extension + ::extensions-store :registry/registry])) diff --git a/src/status_im/ui/screens/events.cljs b/src/status_im/ui/screens/events.cljs index 6550b27eb2..efb002a5fe 100644 --- a/src/status_im/ui/screens/events.cljs +++ b/src/status_im/ui/screens/events.cljs @@ -38,7 +38,7 @@ (defn- http-get [{:keys [url response-validator success-event-creator failure-event-creator timeout-ms]}] (let [on-success #(re-frame/dispatch (success-event-creator %)) - on-error #(re-frame/dispatch (failure-event-creator %)) + on-error (when failure-event-creator #(re-frame/dispatch (failure-event-creator %))) opts {:valid-response? response-validator :timeout-ms timeout-ms}] (http/get url on-success on-error opts))) diff --git a/src/status_im/ui/screens/extensions/add/views.cljs b/src/status_im/ui/screens/extensions/add/views.cljs index 8400e16243..9061b8216f 100644 --- a/src/status_im/ui/screens/extensions/add/views.cljs +++ b/src/status_im/ui/screens/extensions/add/views.cljs @@ -2,8 +2,11 @@ (:require-macros [status-im.utils.views :as views]) (:require [re-frame.core :as re-frame] [clojure.string :as string] + [status-im.extensions.core :as extensions] [status-im.i18n :as i18n] + [status-im.ui.components.react :as react] [status-im.ui.components.colors :as colors] + [status-im.ui.components.styles :as components.styles] [status-im.ui.components.common.common :as components.common] [status-im.ui.components.icons.vector-icons :as vector-icons] [status-im.ui.components.react :as react] @@ -13,49 +16,66 @@ [status-im.ui.components.text-input.view :as text-input] [status-im.ui.screens.extensions.add.styles :as styles])) -(defn cartouche [{:keys [header]} content] +(defn cartouche [{:keys [header]} content] [react/view {:style styles/cartouche-container} [react/text {:style styles/cartouche-header} header] [react/view {:style styles/cartouche-content-wrapper} [react/view {:flex 1} - [react/text {:style styles/text} - content]]]]) + content]]]) (defn hooks [{:keys [hooks]}] (mapcat (fn [[hook-id values]] (map (fn [[id]] - (symbol "hook" (str (name hook-id) "." (name id)))) + (str (name hook-id) "." (name id))) values)) hooks)) (views/defview show-extension [] (views/letsubs [{:keys [data errors]} [:get-staged-extension]] - [react/view styles/screen - [status-bar/status-bar] - [react/keyboard-avoiding-view components.styles/flex - [toolbar/simple-toolbar (i18n/label :t/extension)] - [react/scroll-view {:keyboard-should-persist-taps :handled} - [react/view styles/wrapper - [cartouche {:header (i18n/label :t/identifier)} - (str (get-in data ['meta :name]))] - [cartouche {:header (i18n/label :t/name)} - (str (get-in data ['meta :name]))] - [cartouche {:header (i18n/label :t/description)} - (str (get-in data ['meta :description]))] - [cartouche {:header (i18n/label :t/hooks)} - (string/join " " (hooks data))] - [cartouche {:header (i18n/label :t/permissions)} - (i18n/label :t/none)] - [cartouche {:header (i18n/label :t/errors)} - (i18n/label :t/none)]]] - [react/view styles/bottom-container - [react/view components.styles/flex] - [components.common/bottom-button - {:forward? true - :label (i18n/label :t/install) - :disabled? (not (empty? errors)) - :on-press #(re-frame/dispatch [:extension/install data])}]]]])) + (if data + [react/view styles/screen + [status-bar/status-bar] + [react/keyboard-avoiding-view components.styles/flex + [toolbar/simple-toolbar (i18n/label :t/extension)] + [react/scroll-view {:keyboard-should-persist-taps :handled} + [react/view styles/wrapper + [cartouche {:header (i18n/label :t/identifier)} + [react/text {:style styles/text} + (str (get-in data ['meta :name]))]] + [cartouche {:header (i18n/label :t/name)} + [react/text {:style styles/text} + (str (get-in data ['meta :name]))]] + [cartouche {:header (i18n/label :t/description)} + [react/text {:style styles/text} + (str (get-in data ['meta :description]))]] + [cartouche {:header (i18n/label :t/hooks)} + (into [react/view] (for [hook (hooks data)] + [react/text {:style styles/text} + (str hook)]))] + [cartouche {:header (i18n/label :t/permissions)} + [react/text {:style styles/text} + (i18n/label :t/none)]] + [cartouche {:header (i18n/label :t/errors)} + (if errors + (into [react/view] (for [error errors] + [react/text {:style styles/text} + (str (name (:pluto.reader.errors/type error)) " " (str (:pluto.reader.errors/value error)))])) + [react/text {:style styles/text} + (i18n/label :t/none)])]]] + [react/view styles/bottom-container + [react/view components.styles/flex] + [components.common/bottom-button + {:forward? true + :label (i18n/label :t/install) + :disabled? (not (empty? errors)) + :on-press #(re-frame/dispatch [:extension/install data])}]]]] + [react/view styles/screen + [status-bar/status-bar] + [react/view {:flex 1} + [toolbar/simple-toolbar (i18n/label :t/extension)] + [react/view {:style {:flex 1 :justify-content :center :align-items :center}} + [react/text (i18n/label :t/invalid-extension)]]]]))) (def qr-code [react/touchable-highlight {:on-press #(re-frame/dispatch [:qr-scanner.ui/scan-qr-code-pressed @@ -86,5 +106,5 @@ [components.common/bottom-button {:forward? true :label (i18n/label :t/find) - :disabled? (string/blank? extension-url) - :on-press #(re-frame/dispatch [:extension/show extension-url])}]]]])) + :disabled? (not (extensions/valid-uri? extension-url)) + :on-press #(re-frame/dispatch [:extension/show (string/trim extension-url)])}]]]])) diff --git a/test/cljs/status_im/test/extensions/core.cljs b/test/cljs/status_im/test/extensions/core.cljs new file mode 100644 index 0000000000..bc926e8cff --- /dev/null +++ b/test/cljs/status_im/test/extensions/core.cljs @@ -0,0 +1,10 @@ +(ns status-im.test.extensions.core + (:require [cljs.test :refer-macros [deftest is testing]] + [status-im.extensions.core :as extensions])) + +(deftest valid-uri? + (is (= false (extensions/valid-uri? nil))) + (is (= false (extensions/valid-uri? "http://get.status.im/extension/ipfs"))) + (is (= false (extensions/valid-uri? " http://get.status.im/extension/ipfs@QmWKqSanV4M4zrd55QMkvDMZEQyuHzCMHpX1Fs3dZeSExv "))) + (is (= true (extensions/valid-uri? " https://get.status.im/extension/ipfs@QmWKqSanV4M4zrd55QMkvDMZEQyuHzCMHpX1Fs3dZeSExv "))) + (is (= true (extensions/valid-uri? "https://get.status.im/extension/ipfs@QmWKqSanV4M4zrd55QMkvDMZEQyuHzCMHpX1Fs3dZeSExv")))) diff --git a/test/cljs/status_im/test/runner.cljs b/test/cljs/status_im/test/runner.cljs index 76892d76c9..4ddd5b79e4 100644 --- a/test/cljs/status_im/test/runner.cljs +++ b/test/cljs/status_im/test/runner.cljs @@ -3,6 +3,7 @@ [status-im.test.contacts.subs] [status-im.test.data-store.chats] [status-im.test.data-store.realm.core] + [status-im.test.extensions.core] [status-im.test.browser.core] [status-im.test.browser.permissions] [status-im.test.wallet.subs] @@ -72,6 +73,7 @@ 'status-im.test.init.core 'status-im.test.data-store.chats 'status-im.test.data-store.realm.core + 'status-im.test.extensions.core 'status-im.test.mailserver.core 'status-im.test.group-chats.core 'status-im.test.node.core diff --git a/translations/en.json b/translations/en.json index 3d6eb7cf22..87d6d3c2ec 100644 --- a/translations/en.json +++ b/translations/en.json @@ -385,6 +385,7 @@ "enter-dapp-url": "Enter a ÐApp URL", "wallet-transaction-total-fee": "Total Fee", "extension": "Extension", + "invalid-extension": "Invalid extension URI", "datetime-day": { "one": "day", "other": "days"