diff --git a/.env b/.env index 5325085e90..7a91a39f3b 100644 --- a/.env +++ b/.env @@ -14,3 +14,4 @@ DEBUG_WEBVIEW=1 INSTABUG_SURVEYS=1 GROUP_CHATS_ENABLED=0 CACHED_WEBVIEWS_ENABLED=1 +EXTENSIONS=1 diff --git a/.env.e2e b/.env.e2e index ba037eb6a2..3d7431a5cf 100644 --- a/.env.e2e +++ b/.env.e2e @@ -9,4 +9,5 @@ POW_TIME=1 DEFAULT_NETWORK=testnet_rpc INSTABUG_TOKEN=758630ed52864cbad9c5eeeac596c60c DEBUG_WEBVIEW=1 -GROUP_CHATS_ENABLED=1 \ No newline at end of file +GROUP_CHATS_ENABLED=1 +EXTENSIONS=0 diff --git a/.env.jenkins b/.env.jenkins index 2641852ad6..98c174045d 100644 --- a/.env.jenkins +++ b/.env.jenkins @@ -13,3 +13,4 @@ DEBUG_WEBVIEW=1 GROUP_CHATS_ENABLED=0 MAINNET_WARNING_ENABLED=1 CACHED_WEBVIEWS_ENABLED=1 +EXTENSIONS=0 diff --git a/.env.nightly b/.env.nightly index c06280ab33..cda9cae687 100644 --- a/.env.nightly +++ b/.env.nightly @@ -12,3 +12,4 @@ INSTABUG_TOKEN=758630ed52864cbad9c5eeeac596c60c DEBUG_WEBVIEW=1 GROUP_CHATS_ENABLED=0 MAINNET_WARNING_ENABLED=1 +EXTENSIONS=1 diff --git a/.env.nightly.staging.fleet b/.env.nightly.staging.fleet index 8c7daf434e..d92899a647 100644 --- a/.env.nightly.staging.fleet +++ b/.env.nightly.staging.fleet @@ -13,3 +13,4 @@ DEBUG_WEBVIEW=1 INSTABUG_SURVEYS=1 GROUP_CHATS_ENABLED=0 MAINNET_WARNING_ENABLED=1 +EXTENSIONS=0 diff --git a/.env.prod b/.env.prod index 5e5ed82008..6031ac5ff5 100644 --- a/.env.prod +++ b/.env.prod @@ -13,4 +13,5 @@ TESTFAIRY_TOKEN=969f6c921cb435cea1d41d1ea3f5b247d6026d55 INSTABUG_TOKEN=758630ed52864cbad9c5eeeac596c60c DEBUG_WEBVIEW=0 GROUP_CHATS_ENABLED=0 -MAINNET_WARNING_ENABLED=1 \ No newline at end of file +MAINNET_WARNING_ENABLED=1 +EXTENSIONS=0 diff --git a/deps.edn b/deps.edn index e3a2c84c18..bcb6213491 100644 --- a/deps.edn +++ b/deps.edn @@ -10,7 +10,8 @@ com.andrewmcveigh/cljs-time {:mvn/version "0.5.2"} com.taoensso/timbre {:mvn/version "4.10.0"} hickory {:mvn/version "0.7.1"} - com.cognitect/transit-cljs {:mvn/version "0.8.248"}} + com.cognitect/transit-cljs {:mvn/version "0.8.248"} + status-im/pluto {:mvn/version "iteration-2-SNAPSHOT"}} :aliases {:dev {:extra-deps diff --git a/project.clj b/project.clj index 85c6c75852..fedfcdc3c5 100644 --- a/project.clj +++ b/project.clj @@ -10,7 +10,8 @@ [com.andrewmcveigh/cljs-time "0.5.2"] [com.taoensso/timbre "4.10.0"] [hickory "0.7.1"] - [com.cognitect/transit-cljs "0.8.248"]] + [com.cognitect/transit-cljs "0.8.248"] + [status-im/pluto "iteration-2-SNAPSHOT"]] :plugins [[lein-cljsbuild "1.1.7"] [lein-re-frisk "0.5.8"] [lein-cljfmt "0.5.7"] diff --git a/src/status_im/chat/commands/core.cljs b/src/status_im/chat/commands/core.cljs index 5f6759fc58..ccb417b822 100644 --- a/src/status_im/chat/commands/core.cljs +++ b/src/status_im/chat/commands/core.cljs @@ -1,6 +1,8 @@ (ns status-im.chat.commands.core - (:require [clojure.set :as set] + (:require [re-frame.core :as re-frame] + [clojure.set :as set] [clojure.string :as string] + [pluto.host :as host] [status-im.constants :as constants] [status-im.chat.constants :as chat-constants] [status-im.chat.commands.protocol :as protocol] @@ -8,6 +10,7 @@ [status-im.chat.models :as chat-model] [status-im.chat.models.input :as input-model] [status-im.chat.models.message :as message-model] + [status-im.utils.handlers :as handlers] [status-im.utils.handlers-macro :as handlers-macro])) (def register @@ -80,7 +83,7 @@ (defn index-commands "Takes collecton of things implementing the command protocol, and correctly indexes them by their composite ids and access scopes." - [commands {:keys [db]}] + [commands] (let [id->command (reduce (fn [acc command] (assoc acc (command-id command) {:type command @@ -100,9 +103,66 @@ access-scopes))) {} id->command)] - {:db (assoc db - :id->command id->command - :access-scope->command-id access-scope->command-id)})) + {:id->command id->command + :access-scope->command-id access-scope->command-id})) + +(defn load-commands + "Takes collection of things implementing the command protocol and db, + correctly indexes them and adds them to db in a way that preserves existing commands" + [commands {:keys [db]}] + (let [{:keys [id->command access-scope->command-id]} (index-commands commands)] + {:db (-> db + (update :id->command merge id->command) + (update :access-scope->command-id #(merge-with (fnil into #{}) % access-scope->command-id)))})) + +(defn remove-command + "Remove command form db, correctly updating all indexes" + [command {:keys [db]}] + (let [id (command-id command)] + {:db (-> db + (update :id->command dissoc id) + (update :access-scope->command-id (fn [access-scope->command-id] + (reduce (fn [acc [scope command-ids-set]] + (if (command-ids-set id) + (if (= 1 (count command-ids-set)) + acc + (assoc acc scope (disj command-ids-set id))) + (assoc acc scope command-ids-set))) + {} + access-scope->command-id))))})) + +(def command-hook + "Hook for extensions" + (reify host/AppHook + (id [_] :commands) + (properties [_] {:scope #{:personal-chats :public-chats} + :description :string + :short-preview :view + :preview :view + :parameters [{:id :keyword + :type {:one-of #{:text :phone :password :number}} + :placeholder :string + :suggestions? :component}]}) + (hook-in [_ id {:keys [description scope parameters preview short-preview]} cofx] + (let [new-command (reify protocol/Command + (id [_] (name id)) + (scope [_] scope) + (description [_] description) + (parameters [_] parameters) + (validate [_ _ _]) + (on-send [_ _ _]) + (on-receive [_ _ _]) + (short-preview [_ props] (short-preview props)) + (preview [_ props] (preview props)))] + (load-commands [new-command] cofx))) + (unhook [_ id {:keys [scope]} {:keys [db] :as cofx}] + (remove-command (get-in db [:id->command [(name id) scope] :type]) cofx)))) + +(handlers/register-handler-fx + :load-commands + [re-frame/trim-v] + (fn [cofx [commands]] + (load-commands commands cofx))) (defn chat-commands "Takes `id->command`, `access-scope->command-id` and `chat` parameters and returns diff --git a/src/status_im/chat/commands/impl/transactions.cljs b/src/status_im/chat/commands/impl/transactions.cljs index 09d8f0a266..1b71db5a47 100644 --- a/src/status_im/chat/commands/impl/transactions.cljs +++ b/src/status_im/chat/commands/impl/transactions.cljs @@ -12,6 +12,7 @@ [status-im.ui.components.chat-preview :as chat-preview] [status-im.ui.components.list.views :as list] [status-im.ui.components.animation :as animation] + [status-im.ui.components.svgimage :as svgimage] [status-im.i18n :as i18n] [status-im.constants :as constants] [status-im.utils.ethereum.core :as ethereum] @@ -40,21 +41,40 @@ #_[react/text {:style transactions-styles/asset-balance} (str (money/internal->formatted amount symbol decimals))]]])) +(defn- render-nft-asset [selected-event-creator] + (fn [{:keys [name symbol amount] :as asset}] + [react/touchable-highlight + {:on-press #(re-frame/dispatch (selected-event-creator (clojure.core/name symbol)))} + [react/view transactions-styles/asset-container + [react/view transactions-styles/asset-main + [react/image {:source (-> asset :icon :source) + :style transactions-styles/asset-icon}] + [react/text {:style transactions-styles/asset-symbol} name]] + [react/text {:style transactions-styles/nft-asset-amount} (money/to-fixed amount)]]])) + (def assets-separator [react/view transactions-styles/asset-separator]) -(defview choose-asset [selected-event-creator] +(defview choose-asset [nft? selected-event-creator] (letsubs [assets [:wallet/visible-assets-with-amount]] [react/view - [list/flat-list {:data (filter #(not (:nft? %)) assets) + [list/flat-list {:data (filter #(if nft? + (:nft? %) + (not (:nft? %))) + assets) :key-fn (comp name :symbol) - :render-fn (render-asset selected-event-creator) + :render-fn (if nft? + (render-nft-asset selected-event-creator) + (render-asset selected-event-creator)) :enableEmptySections true :separator assets-separator :keyboardShouldPersistTaps :always :bounces false}]])) (defn choose-asset-suggestion [selected-event-creator] - [choose-asset selected-event-creator]) + [choose-asset false selected-event-creator]) + +(defn choose-nft-asset-suggestion [selected-event-creator] + [choose-asset true selected-event-creator]) (defn personal-send-request-short-preview [label-key {:keys [content]}] @@ -77,6 +97,32 @@ :type :number :placeholder "Amount"}]) +(defview choose-nft-token [selected-event-creator] + (letsubs [{:keys [input-params]} [:selected-chat-command] + collectibles [:collectibles]] + (let [collectible-tokens (get collectibles (keyword (:symbol input-params)))] + [react/view {:flex-direction :row + :align-items :center + :padding-vertical 11} + (map + (fn [[id {:keys [name image_url]}]] + [react/touchable-highlight + {:key id + :on-press #(re-frame/dispatch (selected-event-creator (str id)))} + [react/view {:flex-direction :column + :align-items :center + :margin-left 10 + :border-radius 2 + :border-width 1 + :border-color colors/gray} + [svgimage/svgimage {:style transactions-styles/nft-token-icon + :source {:uri image_url}}] + [react/text {} name]]]) + collectible-tokens)]))) + +(defn choose-nft-token-suggestion [selected-event-creator] + [choose-nft-token selected-event-creator]) + ;;TODO(goranjovic): currently we only allow tokens which are enabled in Manage assets here ;; because balances are only fetched for them. Revisit this decision with regard to battery/network consequences ;; if we were to update all balances. diff --git a/src/status_im/chat/commands/impl/transactions/styles.cljs b/src/status_im/chat/commands/impl/transactions/styles.cljs index 375488d0df..ad127ac441 100644 --- a/src/status_im/chat/commands/impl/transactions/styles.cljs +++ b/src/status_im/chat/commands/impl/transactions/styles.cljs @@ -23,6 +23,17 @@ (def asset-symbol {:color colors/black}) +(def nft-asset-amount + {:font-size 16 + :color colors/gray + :padding-right 14}) + +(def nft-token-icon + {:width 100 + :height 100 + :margin-left 20 + :margin-right 20}) + (def asset-name {:color colors/gray :padding-left 4}) diff --git a/src/status_im/chat/commands/sending.cljs b/src/status_im/chat/commands/sending.cljs index ea442c3a5a..390e653a76 100644 --- a/src/status_im/chat/commands/sending.cljs +++ b/src/status_im/chat/commands/sending.cljs @@ -25,7 +25,8 @@ :prefill [(get parameter-map :asset) (get parameter-map :amount)]} :content-type constants/content-type-command-request}} - path)) + path + {:content-type constants/content-type-command})) (defn- create-command-message "Create message map from chat-id, command & input parameters" diff --git a/src/status_im/chat/styles/message/message.cljs b/src/status_im/chat/styles/message/message.cljs index 35cc61d1b8..e66d32de37 100644 --- a/src/status_im/chat/styles/message/message.cljs +++ b/src/status_im/chat/styles/message/message.cljs @@ -157,56 +157,10 @@ {:padding-top 12 :padding-bottom 10}))) -(def author - {:color styles/color-gray4 - :margin-bottom 4 - :font-size 12}) - -(def audio-container - {:flex-direction :row - :align-items :center}) - -(def play-view - {:width 33 - :height 33 - :border-radius 16 - :elevation 1}) - (def play-image {:width 33 :height 33}) -(def track-container - {:margin-top 10 - :margin-left 10 - :width 120 - :height 26 - :elevation 1}) - -(def track - {:position :absolute - :top 4 - :width 120 - :height 2 - :background-color :#EC7262}) - -(def track-mark - {:position :absolute - :left 0 - :top 0 - :width 2 - :height 10 - :background-color :#4A5258}) - -(def track-duration-text - {:position :absolute - :left 1 - :top 11 - :font-size 11 - :color :#4A5258 - :letter-spacing 1 - :line-height 15}) - (def status-container {:flex 1 :align-self :center diff --git a/src/status_im/chat/subs.cljs b/src/status_im/chat/subs.cljs index cf9b082c8c..a1f2ec4150 100644 --- a/src/status_im/chat/subs.cljs +++ b/src/status_im/chat/subs.cljs @@ -280,10 +280,12 @@ :<- [:chat-parameter-box] :<- [:show-suggestions?] :<- [:validation-messages] - (fn [[chat-parameter-box show-suggestions? validation-messages]] + :<- [:selected-chat-command] + (fn [[chat-parameter-box show-suggestions? validation-messages {:keys [command-completion]}]] (and chat-parameter-box (not validation-messages) - (not show-suggestions?)))) + (not show-suggestions?) + (not (= :complete command-completion))))) (reg-sub :show-suggestions-view? diff --git a/src/status_im/chat/views/message/message.cljs b/src/status_im/chat/views/message/message.cljs index 79633ec387..f722a3b482 100644 --- a/src/status_im/chat/views/message/message.cljs +++ b/src/status_im/chat/views/message/message.cljs @@ -42,22 +42,12 @@ :font :default} status])]))) -(defn message-content-audio [_] - [react/view style/audio-container - [react/view style/play-view - [react/image {:style style/play-image}]] - [react/view style/track-container - [react/view style/track] - [react/view style/track-mark] - [react/text {:style style/track-duration-text - :font :default} - "03:39"]]]) - (defview message-content-command [command-message] (letsubs [id->command [:get-id->command]] - (when-let [command (commands-receiving/lookup-command-by-ref command-message id->command)] - (commands/generate-preview command command-message)))) + (if-let [command (commands-receiving/lookup-command-by-ref command-message id->command)] + (commands/generate-preview command command-message) + [react/text (str "Unhandled command: " (-> command-message :content :command-path first))]))) (def rtl-characters-regex #"[^\u0591-\u06EF\u06FA-\u07FF\u200F\u202B\u202E\uFB1D-\uFDFD\uFE70-\uFEFC]*?[\u0591-\u06EF\u06FA-\u07FF\u200F\u202B\u202E\uFB1D-\uFDFD\uFE70-\uFEFC]") @@ -235,11 +225,10 @@ [wrapper message [emoji-message message]]) (defmethod message-content :default - [wrapper {:keys [content-type content] :as message}] + [wrapper {:keys [content-type] :as message}] [wrapper message [message-view message - [message-content-audio {:content content - :content-type content-type}]]]) + [react/text {} (str "Unhandled content-type " content-type)]]]) (defn- text-status [status] [react/view style/delivery-view diff --git a/src/status_im/extensions/core.cljs b/src/status_im/extensions/core.cljs new file mode 100644 index 0000000000..b068d150b0 --- /dev/null +++ b/src/status_im/extensions/core.cljs @@ -0,0 +1,7 @@ +(ns status-im.extensions.core + (:require [clojure.string :as string])) + +(defn url->storage-details [s] + (when s + (let [[_ type id] (string/split s #".*[:/]([a-z]*)@(.*)")] + [(keyword type) id]))) diff --git a/src/status_im/extensions/registry.cljs b/src/status_im/extensions/registry.cljs new file mode 100644 index 0000000000..184654c017 --- /dev/null +++ b/src/status_im/extensions/registry.cljs @@ -0,0 +1,52 @@ +(ns status-im.extensions.registry + (:require [pluto.reader :as reader] + [pluto.registry :as registry] + [pluto.host :as host] + [pluto.storage :as storage] + [pluto.storage.gist :as gist] + [status-im.extensions.core :as extension] + [status-im.chat.commands.core :as commands] + [status-im.chat.commands.impl.transactions :as transactions] + [status-im.ui.components.react :as react])) + +(def components + {'view react/view + 'text react/text + 'asset-selector transactions/choose-nft-asset-suggestion + 'token-selector transactions/choose-nft-token-suggestion}) + +(def app-hooks #{commands/command-hook}) + +(def capacities + (reduce (fn [capacities hook] + (assoc-in capacities [:hooks (host/id hook)] hook)) + {:components components + :queries #{:get-in} + :events #{:set-in} + :permissions {:read {:include-paths #{[:network] + [:current-chat-id] + [:chats #".*"]}} + :write {:include-paths #{}}}} + app-hooks)) + +(defn parse [{:keys [data]}] + (try + (let [{:keys [errors] :as extension-data} (reader/parse {:capacities capacities} data)] + (when errors + (println "Failed to parse status extensions" errors)) + extension-data) + (catch :default e (println "EXC" e)))) + +(def storages + {:gist (gist/GistStorage.)}) + +(defn read-extension [o] + (-> o :value first :content reader/read)) + +(defn load-from [url f] + (let [[type id] (extension/url->storage-details url) + storage (get storages type)] + (when (and storage id) + (storage/fetch storage + {:value id} + #(f %))))) diff --git a/src/status_im/models/chat.cljs b/src/status_im/models/chat.cljs index d95c279db1..515b4d0bd2 100644 --- a/src/status_im/models/chat.cljs +++ b/src/status_im/models/chat.cljs @@ -72,7 +72,7 @@ :contacts/dapps default-dapps)} (group-chat-messages) (add-default-contacts) - (commands/index-commands commands/register)))) + (commands/load-commands commands/register)))) (defn process-pending-messages "Change status of own messages which are still in `sending` status to `not-sent` diff --git a/src/status_im/translations/en.cljs b/src/status_im/translations/en.cljs index bd7ffaa25d..e06ff47f6c 100644 --- a/src/status_im/translations/en.cljs +++ b/src/status_im/translations/en.cljs @@ -12,6 +12,9 @@ :mailserver-reconnect "Could not connect to mailserver. Tap to reconnect" :fetching-messages "Fetching messages..." :search-for "Search for..." + :find "Find" + :install "Install" + :success "Success" :cancel "Cancel" :next "Next" :open "Open" @@ -710,6 +713,18 @@ :network-id "Network ID" :specify-network-id "Specify network id" + :extension "Extension" + :extensions "Extensions" + :extension-installed "You installed an extension" + :extension-find "Find extension" + :extension-address "Extension address" + :extension-url "Enter an extension URL" + :no-extension "No extension installed" + :identifier "Identifier" + :errors "Errors" + :hooks "Hooks" + :permissions "Permissions" + ;; invalid-key :invalid-key-title "We detected a problem with the encryption key" diff --git a/src/status_im/ui/screens/db.cljs b/src/status_im/ui/screens/db.cljs index 26e90c1700..d7d6cb1b7c 100644 --- a/src/status_im/ui/screens/db.cljs +++ b/src/status_im/ui/screens/db.cljs @@ -4,6 +4,7 @@ [status-im.constants :as constants] [status-im.utils.platform :as platform] [status-im.utils.dimensions :as dimensions] + pluto.registry status-im.transport.db status-im.ui.screens.accounts.db status-im.ui.screens.contacts.db @@ -55,7 +56,8 @@ :tooltips {} :desktop/desktop {:tab-view-id :home} :dimensions/window (dimensions/window) - :push-notifications/stored {}}) + :push-notifications/stored {} + :registry {}}) ;;;;GLOBAL @@ -120,6 +122,8 @@ (spec/def :navigation.screen-params/collectibles-list map?) +(spec/def :navigation.screen-params/show-extension map?) + (spec/def :navigation/screen-params (spec/nilable (allowed-keys :opt-un [:navigation.screen-params/network-details :navigation.screen-params/browser :navigation.screen-params/profile-qr-viewer @@ -127,7 +131,8 @@ :navigation.screen-params/group-contacts :navigation.screen-params/edit-contact-group :navigation.screen-params/dapp-description - :navigation.screen-params/collectibles-list]))) + :navigation.screen-params/collectibles-list + :navigation.screen-params/show-extension]))) (spec/def :desktop/desktop (spec/nilable any?)) (spec/def ::tooltips (spec/nilable any?)) @@ -144,6 +149,9 @@ (spec/def ::collectible (spec/nilable map?)) (spec/def ::collectibles (spec/nilable map?)) +(spec/def ::extension-url (spec/nilable string?)) +(spec/def ::staged-extension (spec/nilable any?)) + ;;;;NODE (spec/def ::message-envelopes (spec/nilable map?)) @@ -284,4 +292,7 @@ :notifications/notifications ::device-UUID ::collectible - ::collectibles])) + ::collectibles + ::extension-url + ::staged-extension + :registry/registry])) diff --git a/src/status_im/ui/screens/events.cljs b/src/status_im/ui/screens/events.cljs index 22adef0cc8..b61561298a 100644 --- a/src/status_im/ui/screens/events.cljs +++ b/src/status_im/ui/screens/events.cljs @@ -26,6 +26,7 @@ status-im.ui.screens.network-settings.events status-im.ui.screens.profile.events status-im.ui.screens.qr-scanner.events + status-im.ui.screens.extensions.events status-im.ui.screens.wallet.events [status-im.models.wallet :as models.wallet] status-im.ui.screens.wallet.collectibles.events diff --git a/src/status_im/ui/screens/extensions/add/events.cljs b/src/status_im/ui/screens/extensions/add/events.cljs new file mode 100644 index 0000000000..64c560b793 --- /dev/null +++ b/src/status_im/ui/screens/extensions/add/events.cljs @@ -0,0 +1,55 @@ +(ns status-im.ui.screens.extensions.add.events + (:require [re-frame.core :as re-frame] + [pluto.registry :as registry] + [status-im.extensions.registry :as extensions] + [status-im.ui.screens.navigation :as navigation] + [status-im.i18n :as i18n] + [status-im.utils.handlers :as handlers] + [status-im.utils.handlers-macro :as handlers-macro])) + +(re-frame/reg-fx + :extension/load + (fn [[url follow-up-event]] + (extensions/load-from url #(re-frame/dispatch [follow-up-event (-> % extensions/read-extension extensions/parse)])))) + +(handlers/register-handler-fx + :extension/install + [re-frame/trim-v] + (fn [cofx [extension-data]] + (let [extension-key (get-in extension-data ['meta :name])] + (handlers-macro/merge-fx cofx + {:show-confirmation {:title (i18n/label :t/success) + :content (i18n/label :t/extension-installed) + :on-accept #(re-frame/dispatch [:navigate-to-clean :home]) + :on-cancel nil}} + (registry/add extension-data) + (registry/activate extension-key))))) + +(handlers/register-handler-db + :extension/edit-address + [re-frame/trim-v] + (fn [db [address]] + (assoc db :extension-url address))) + +(handlers/register-handler-db + :extension/stage + [re-frame/trim-v] + (fn [db [extension-data]] + (-> db + (assoc :staged-extension extension-data) + (navigation/navigate-to :show-extension)))) + +(handlers/register-handler-fx + :extension/show + [re-frame/trim-v] + (fn [cofx [uri]] + {:extension/load [uri :extension/stage]})) + +(handlers/register-handler-fx + :extension/toggle-activation + [re-frame/trim-v] + (fn [cofx [id state]] + (when-let [toggle-fn (get {true registry/activate + false registry/deactivate} + state)] + (toggle-fn id cofx)))) diff --git a/src/status_im/ui/screens/extensions/add/styles.cljs b/src/status_im/ui/screens/extensions/add/styles.cljs new file mode 100644 index 0000000000..5f724bee7c --- /dev/null +++ b/src/status_im/ui/screens/extensions/add/styles.cljs @@ -0,0 +1,51 @@ +(ns status-im.ui.screens.extensions.add.styles + (:require-macros [status-im.utils.styles :refer [defstyle]]) + (:require [status-im.ui.components.styles :as styles] + [status-im.ui.components.colors :as colors])) + +(def wrapper + {:flex 1 + :margin 16}) + +(def input-container + {:flex-direction :row + :align-items :center + :justify-content :space-between + :border-radius styles/border-radius + :height 52 + :margin-top 15}) + +(defstyle input + {:flex 1 + :font-size 15 + :letter-spacing -0.2 + :android {:padding 0}}) + +(def bottom-container + {:flex-direction :row + :margin-horizontal 12 + :margin-vertical 15}) + +(def hooks + {:margin-top 20 + :margin-left 10}) + +(def text + {:color colors/black}) + +(def cartouche-container + {:flex 1 + :margin-top 16 + :margin-horizontal 16}) + +(def cartouche-header + {:color colors/gray}) + +(def cartouche-content-wrapper + {:flex-direction :row + :margin-top 8 + :border-color colors/gray-lighter + :border-width 1 + :border-radius styles/border-radius + :padding 16 + :background-color colors/white-transparent}) diff --git a/src/status_im/ui/screens/extensions/add/subs.cljs b/src/status_im/ui/screens/extensions/add/subs.cljs new file mode 100644 index 0000000000..39a8bde0ab --- /dev/null +++ b/src/status_im/ui/screens/extensions/add/subs.cljs @@ -0,0 +1,12 @@ +(ns status-im.ui.screens.extensions.add.subs + (:require [re-frame.core :as re-frame])) + +(re-frame/reg-sub + :get-extension-url + (fn [db] + (:extension-url db))) + +(re-frame/reg-sub + :get-staged-extension + (fn [db] + (:staged-extension db))) diff --git a/src/status_im/ui/screens/extensions/add/views.cljs b/src/status_im/ui/screens/extensions/add/views.cljs new file mode 100644 index 0000000000..e23021253e --- /dev/null +++ b/src/status_im/ui/screens/extensions/add/views.cljs @@ -0,0 +1,78 @@ +(ns status-im.ui.screens.extensions.add.views + (:require-macros [status-im.utils.views :as views]) + (:require [re-frame.core :as re-frame] + [clojure.string :as string] + [status-im.ui.components.react :as react] + [status-im.i18n :as i18n] + [status-im.ui.components.styles :as components.styles] + [status-im.ui.components.common.common :as components.common] + [status-im.ui.components.status-bar.view :as status-bar] + [status-im.ui.components.toolbar.view :as toolbar] + [status-im.ui.components.text-input.view :as text-input] + [status-im.ui.screens.extensions.add.styles :as styles])) + +(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]]]]) + +(defn hooks [{:keys [hooks]}] + (mapcat (fn [[hook-id values]] + (map (fn [[id]] + (symbol "hook" (str (name hook-id) "." (name id)))) + values)) + hooks)) + +(views/defview show-extension [] + (views/letsubs [{:keys [data errors]} [:get-staged-extension]] + [react/view components.styles/flex + [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? (seq errors) + :on-press #(re-frame/dispatch [:extension/install data])}]]]])) + +(views/defview add-extension [] + (views/letsubs [extension-url [:get-extension-url]] + [react/view components.styles/flex + [status-bar/status-bar] + [react/keyboard-avoiding-view components.styles/flex + [toolbar/simple-toolbar (i18n/label :t/extension-find)] + [react/scroll-view {:keyboard-should-persist-taps :handled} + [react/view styles/wrapper + [text-input/text-input-with-label + {:label (i18n/label :t/extension-address) + :style styles/input + :container styles/input-container + :placeholder (i18n/label :t/extension-url) + :on-change-text #(re-frame/dispatch [:extension/edit-address %])}]]] + [react/view styles/bottom-container + [react/view components.styles/flex] + [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])}]]]])) diff --git a/src/status_im/ui/screens/extensions/events.cljs b/src/status_im/ui/screens/extensions/events.cljs new file mode 100644 index 0000000000..869a50d296 --- /dev/null +++ b/src/status_im/ui/screens/extensions/events.cljs @@ -0,0 +1,12 @@ +(ns status-im.ui.screens.extensions.events + (:require [re-frame.core :as re-frame] + [pluto.registry :as registry] + [status-im.utils.handlers :as handlers] + status-im.ui.screens.extensions.add.events)) + +(handlers/register-handler-db + :extensions/toggle-activation + [re-frame/trim-v] + (fn [db [id m]] + nil)) + diff --git a/src/status_im/ui/screens/extensions/styles.cljs b/src/status_im/ui/screens/extensions/styles.cljs new file mode 100644 index 0000000000..8f88afeb53 --- /dev/null +++ b/src/status_im/ui/screens/extensions/styles.cljs @@ -0,0 +1,21 @@ +(ns status-im.ui.screens.extensions.styles + (:require [status-im.ui.components.colors :as colors])) + +(def wrapper + {:flex 1 + :background-color colors/white}) + +(defn wnode-icon [connected?] + {:width 40 + :height 40 + :border-radius 20 + :background-color (if connected? + colors/blue + colors/gray-light) + :align-items :center + :justify-content :center}) + +(def empty-list + {:color colors/black + :text-align :center}) + diff --git a/src/status_im/ui/screens/extensions/subs.cljs b/src/status_im/ui/screens/extensions/subs.cljs new file mode 100644 index 0000000000..915bdd6a20 --- /dev/null +++ b/src/status_im/ui/screens/extensions/subs.cljs @@ -0,0 +1,8 @@ +(ns status-im.ui.screens.extensions.subs + (:require [re-frame.core :as re-frame] + status-im.ui.screens.extensions.add.subs)) + +(re-frame/reg-sub + :get-extensions + (fn [db] + (seq (:registry db)))) diff --git a/src/status_im/ui/screens/extensions/views.cljs b/src/status_im/ui/screens/extensions/views.cljs new file mode 100644 index 0000000000..d25b72b04f --- /dev/null +++ b/src/status_im/ui/screens/extensions/views.cljs @@ -0,0 +1,47 @@ +(ns status-im.ui.screens.extensions.views + (:require-macros [status-im.utils.views :as views]) + (:require [re-frame.core :as re-frame] + [status-im.i18n :as i18n] + [status-im.extensions.registry :as registry] + [status-im.ui.components.icons.vector-icons :as vector-icons] + [status-im.ui.components.list.views :as list] + [status-im.ui.components.react :as react] + [status-im.ui.components.status-bar.view :as status-bar] + [status-im.ui.components.toolbar.view :as toolbar] + [status-im.ui.components.toolbar.actions :as toolbar.actions] + [status-im.ui.screens.extensions.styles :as styles])) + +(def wnode-icon + [react/view (styles/wnode-icon true) + [vector-icons/icon :icons/wnode {:color :white}]]) + +(defn navigate-to-add-extension [wnode-id] + (re-frame/dispatch [:navigate-to :add-extension wnode-id])) + +(defn- render-extension [[id {:keys [state]}]] + [list/list-item-with-checkbox + {:checked? (= :active state) + :on-value-change #(re-frame/dispatch [:extension/toggle-activation id %])} + [list/item + wnode-icon + [list/item-content + [list/item-primary id] + [list/item-secondary id]]]]) + +(views/defview extensions-settings [] + (views/letsubs [extensions [:get-extensions]] + [react/view {:flex 1} + [status-bar/status-bar] + [toolbar/toolbar {} + toolbar/default-nav-back + [toolbar/content-title (i18n/label :t/extensions)] + [toolbar/actions + [(toolbar.actions/add false (partial navigate-to-add-extension nil))]]] + [react/view styles/wrapper + [list/flat-list {:data extensions + :default-separator? false + :key-fn first + :render-fn render-extension + :content-container-style (merge (when (zero? (count extensions)) {:flex-grow 1}) {:justify-content :center}) + :empty-component [react/text {:style styles/empty-list} + (i18n/label :t/no-extension)]}]]])) diff --git a/src/status_im/ui/screens/profile/user/views.cljs b/src/status_im/ui/screens/profile/user/views.cljs index dd8a554b45..3a3199146a 100644 --- a/src/status_im/ui/screens/profile/user/views.cljs +++ b/src/status_im/ui/screens/profile/user/views.cljs @@ -145,6 +145,11 @@ (letsubs [{:keys [sharing-usage-data?]} [:get-current-account]] {:component-did-mount on-show} [react/view + (when (and config/extensions-enabled? dev-mode?) + [profile.components/settings-item + {:label-kw :t/extensions + :action-fn #(re-frame/dispatch [:navigate-to :extensions-settings]) + :accessibility-label :extensions-button}]) (when dev-mode? [profile.components/settings-item {:label-kw :t/network diff --git a/src/status_im/ui/screens/subs.cljs b/src/status_im/ui/screens/subs.cljs index 2b850743ec..d5bc2572a3 100644 --- a/src/status_im/ui/screens/subs.cljs +++ b/src/status_im/ui/screens/subs.cljs @@ -3,6 +3,7 @@ [status-im.utils.ethereum.core :as ethereum] status-im.chat.subs status-im.ui.screens.accounts.subs + status-im.ui.screens.extensions.subs status-im.ui.screens.home.subs status-im.ui.screens.contacts.subs status-im.ui.screens.group.subs diff --git a/src/status_im/ui/screens/views.cljs b/src/status_im/ui/screens/views.cljs index 7671efb342..d2727c754b 100644 --- a/src/status_im/ui/screens/views.cljs +++ b/src/status_im/ui/screens/views.cljs @@ -40,8 +40,10 @@ [status-im.ui.screens.network-settings.views :refer [network-settings]] [status-im.ui.screens.network-settings.network-details.views :refer [network-details]] [status-im.ui.screens.network-settings.edit-network.views :refer [edit-network]] + [status-im.ui.screens.extensions.views :refer [extensions-settings]] [status-im.ui.screens.offline-messaging-settings.views :refer [offline-messaging-settings]] [status-im.ui.screens.offline-messaging-settings.edit-mailserver.views :refer [edit-mailserver]] + [status-im.ui.screens.extensions.add.views :refer [add-extension show-extension]] [status-im.ui.screens.bootnodes-settings.views :refer [bootnodes-settings]] [status-im.ui.screens.bootnodes-settings.edit-bootnode.views :refer [edit-bootnode]] [status-im.ui.screens.currency-settings.views :refer [currency-settings]] @@ -87,10 +89,13 @@ :login login :recover recover :network-settings network-settings + :extensions-settings extensions-settings :network-details network-details :edit-network edit-network :offline-messaging-settings offline-messaging-settings :edit-mailserver edit-mailserver + :add-extension add-extension + :show-extension show-extension :bootnodes-settings bootnodes-settings :edit-bootnode edit-bootnode :currency-settings currency-settings diff --git a/src/status_im/ui/screens/wallet/collectibles/events.cljs b/src/status_im/ui/screens/wallet/collectibles/events.cljs index 98372a889b..6eff3889b9 100644 --- a/src/status_im/ui/screens/wallet/collectibles/events.cljs +++ b/src/status_im/ui/screens/wallet/collectibles/events.cljs @@ -26,7 +26,7 @@ (defn load-token [web3 i items-number contract address symbol] (when (< i items-number) (erc721/token-of-owner-by-index web3 contract address i - (fn [v1 v2] + (fn [_ v2] (load-token web3 (inc i) items-number contract address symbol) (re-frame/dispatch [:load-collectible symbol (.toNumber v2)]))))) diff --git a/src/status_im/utils/config.cljs b/src/status_im/utils/config.cljs index 30b52304bc..482a49642e 100644 --- a/src/status_im/utils/config.cljs +++ b/src/status_im/utils/config.cljs @@ -26,6 +26,7 @@ (def in-app-notifications-enabled? (enabled? (get-config :IN_APP_NOTIFICATIONS_ENABLED 0))) (def cached-webviews-enabled? (enabled? (get-config :CACHED_WEBVIEWS_ENABLED 0))) (def rn-bridge-threshold-warnings-enabled? (enabled? (get-config :RN_BRIDGE_THRESHOLD_WARNINGS 0))) +(def extensions-enabled? (enabled? (get-config :EXTENSIONS 0))) ;; CONFIG VALUES (def log-level diff --git a/src/status_im/utils/universal_links/core.cljs b/src/status_im/utils/universal_links/core.cljs index be7d44a30a..d6191f1690 100644 --- a/src/status_im/utils/universal_links/core.cljs +++ b/src/status_im/utils/universal_links/core.cljs @@ -18,6 +18,7 @@ (def public-chat-regex #".*/chat/public/(.*)$") (def profile-regex #".*/user/(.*)$") (def browse-regex #".*/browse/(.*)$") +(def extension-regex #".*/extension/(.*)$") (defn match-url [url regex] (some->> url @@ -50,6 +51,10 @@ (navigation/navigate-to-cofx :my-profile nil cofx) (chat.events/show-profile profile-id true cofx))) +(defn handle-extension [url cofx] + (log/info "universal-links: handling url profile" url) + {:extension/load [url :extensions/stage]}) + (defn handle-not-found [full-url] (log/info "universal-links: no handler for " full-url)) @@ -73,6 +78,9 @@ (match-url url browse-regex) (handle-browse url cofx) + (match-url url extension-regex) + (handle-extension url cofx) + :else (handle-not-found url))) (defn store-url-for-later diff --git a/test/cljs/status_im/test/chat/commands/core.cljs b/test/cljs/status_im/test/chat/commands/core.cljs index b081323535..53276fe0e9 100644 --- a/test/cljs/status_im/test/chat/commands/core.cljs +++ b/test/cljs/status_im/test/chat/commands/core.cljs @@ -64,8 +64,8 @@ (def TestCommandInstance (TestCommand.)) (def AnotherTestCommandInstance (AnotherTestCommand.)) -(deftest index-commands-test - (let [fx (core/index-commands #{TestCommandInstance AnotherTestCommandInstance} {:db {}})] +(deftest load-commands-test + (let [fx (core/load-commands #{TestCommandInstance AnotherTestCommandInstance} {:db {}})] (testing "Primary composite key index for command is correctly created" (is (= TestCommandInstance (get-in fx [:db :id->command @@ -94,7 +94,7 @@ (core/command-id AnotherTestCommandInstance)))))) (deftest chat-commands-test - (let [fx (core/index-commands #{TestCommandInstance AnotherTestCommandInstance} {:db {}})] + (let [fx (core/load-commands #{TestCommandInstance AnotherTestCommandInstance} {:db {}})] (testing "That relevant commands are looked up for chat" (is (= #{TestCommandInstance AnotherTestCommandInstance} (into #{} @@ -119,7 +119,7 @@ {:chat-id "contact"}))))))) (deftest selected-chat-command-test - (let [fx (core/index-commands #{TestCommandInstance AnotherTestCommandInstance} {:db {}}) + (let [fx (core/load-commands #{TestCommandInstance AnotherTestCommandInstance} {:db {}}) commands (core/chat-commands (get-in fx [:db :id->command]) (get-in fx [:db :access-scope->command-id]) {:chat-id "contact"})]