diff --git a/nix/deps/gradle/proj.list b/nix/deps/gradle/proj.list index 39e5af6b067..17a6822c8f7 100644 --- a/nix/deps/gradle/proj.list +++ b/nix/deps/gradle/proj.list @@ -38,5 +38,6 @@ react-native-share react-native-status react-native-status-keycard react-native-svg +react-native-view-shot react-native-webview walletconnect_react-native-compat diff --git a/package.json b/package.json index ac06b3ea422..210b189eafd 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "node-libs-react-native": "^1.2.1", "react": "18.2.0", "react-dom": "18.0.0", + "react-freeze": "^1.0.4", "react-native": "0.73.5", "react-native-background-timer": "^2.1.1", "react-native-biometrics": "^3.0.1", @@ -71,6 +72,7 @@ "react-native-share": "10.0.2", "react-native-status-keycard": "git+https://github.com/status-im/react-native-status-keycard.git#refs/tags/v2.6.6", "react-native-svg": "13.10.0", + "react-native-view-shot": "^3.8.0", "react-native-webview": "13.6.3", "react-syntax-highlighter": "^15.5.0" }, diff --git a/src/js/worklets/browser.js b/src/js/worklets/browser.js new file mode 100644 index 00000000000..d8457e0dfd6 --- /dev/null +++ b/src/js/worklets/browser.js @@ -0,0 +1,7 @@ +import { useDerivedValue, useSharedValue, scrollTo } from 'react-native-reanimated'; + +export function useScrollTab({ animatedRef, xTranslation, animate }) { + useDerivedValue(() => { + scrollTo(animatedRef, xTranslation.value, 0, animate); + }); +} diff --git a/src/legacy/status_im/ui/screens/browser/stack.cljs b/src/legacy/status_im/ui/screens/browser/stack.cljs index 153b21f609c..b8a4bf16aca 100644 --- a/src/legacy/status_im/ui/screens/browser/stack.cljs +++ b/src/legacy/status_im/ui/screens/browser/stack.cljs @@ -5,14 +5,16 @@ [legacy.status-im.ui.screens.browser.views :as browser] [react-native.core :as rn] [react-native.safe-area :as safe-area] + [status-im.contexts.browser.view :as new-browser] [utils.re-frame :as rf])) (defn browser-stack [] (let [screen-id (rf/sub [:browser/screen-id])] [rn/view {:padding-top safe-area/top :flex 1} - (case screen-id - :empty-tab [empty-tab/empty-tab] - :browser [browser/browser] - :browser-tabs [tabs/tabs] - [empty-tab/empty-tab])])) + [new-browser/view] + #_(case screen-id + :empty-tab [empty-tab/empty-tab] + :browser [browser/browser] + :browser-tabs [tabs/tabs] + [empty-tab/empty-tab])])) diff --git a/src/quo/components/navigation/top_nav/view.cljs b/src/quo/components/navigation/top_nav/view.cljs index 7b133582162..9b58c0269f7 100644 --- a/src/quo/components/navigation/top_nav/view.cljs +++ b/src/quo/components/navigation/top_nav/view.cljs @@ -128,8 +128,7 @@ :blur? true/false :theme :light/:dark :notification :mention/:seen/:notification (TODO :mention-seen temporarily used while resolving https://github.com/status-im/status-mobile/issues/17102 ) - :avatar-props qu2/user-avatar props - :avatar-on-press callback + :avatar-props qu2/user-avatar props :avatar-on-press callback :scan-on-press callback :activity-center-on-press callback :qr-code-on-press callback diff --git a/src/react_native/freeze.cljs b/src/react_native/freeze.cljs new file mode 100644 index 00000000000..9f0af7f0120 --- /dev/null +++ b/src/react_native/freeze.cljs @@ -0,0 +1,5 @@ +(ns react-native.freeze + (:require ["react-freeze" :refer [Freeze]] + [reagent.core :as reagent])) + +(def view (reagent/adapt-react-class Freeze)) diff --git a/src/react_native/view_shot.cljs b/src/react_native/view_shot.cljs new file mode 100644 index 00000000000..077fc60b115 --- /dev/null +++ b/src/react_native/view_shot.cljs @@ -0,0 +1,15 @@ +(ns react-native.view-shot + (:require ["react-native-view-shot" :as view-shot] + [reagent.core :as reagent])) + +(def view (reagent/adapt-react-class view-shot/default)) + +(defn capture + [^js ref] + (some-> ^js ref + (.capture))) + +(defn release-capture + [^js ref uri] + (some-> ^js ref + (.releaseCapture uri))) diff --git a/src/react_native/webview.cljs b/src/react_native/webview.cljs new file mode 100644 index 00000000000..4934dd4a564 --- /dev/null +++ b/src/react_native/webview.cljs @@ -0,0 +1,27 @@ +(ns react-native.webview + (:require + ["react-native-webview" :default rn-webview] + [reagent.core :as reagent] + [utils.transforms :as transforms])) + +(def view (reagent/adapt-react-class rn-webview)) + +(defn go-back + [^js webview-ref] + (some-> ^js webview-ref + (.goBack))) + +(defn go-forward + [^js webview-ref] + (some-> ^js webview-ref + (.goForward))) + +(defn inject-js + [^js webview-ref js-script] + (some-> webview-ref + (.injectJavaScript js-script))) + +(defn post-message + [^js webview-ref message] + (some-> webview-ref + (.postMessage (-> message transforms/clj->json)))) diff --git a/src/status_im/common/signals/events.cljs b/src/status_im/common/signals/events.cljs index 9a6d8eb4144..58b0fc77ec4 100644 --- a/src/status_im/common/signals/events.cljs +++ b/src/status_im/common/signals/events.cljs @@ -1,5 +1,6 @@ (ns status-im.common.signals.events (:require + [camel-snake-kebab.extras :as cske] [legacy.status-im.chat.models.message :as models.message] [legacy.status-im.mailserver.core :as mailserver] [legacy.status-im.visibility-status-updates.core :as visibility-status-updates] @@ -45,11 +46,10 @@ "wallet.router.transactions-sent" {:fx [[:dispatch [:wallet/transactions-sent-signal-received (transforms/js->clj event-js)]]]} - "envelope.sent" - (messages.transport/update-envelopes-status - cofx - (:ids (transforms/js->clj event-js)) - :sent) + "envelope.sent" (messages.transport/update-envelopes-status + cofx + (:ids (transforms/js->clj event-js)) + :sent) "envelope.expired" (messages.transport/update-envelopes-status @@ -110,4 +110,39 @@ "backup.performed" {:db (assoc-in db [:profile/profile :last-backup] (oops/oget event-js :lastBackup))} + "connector.sendRequestAccounts" + {:fx [[:dispatch + [:browser.rpc/on-request-accounts-signal + (->> event-js + transforms/js->clj + (cske/transform-keys transforms/->kebab-case-keyword))]]]} + + "connector.dAppPermissionGranted" + {:fx [[:dispatch + [:browser.rpc/on-permission-granted-signal + (->> event-js + transforms/js->clj + (cske/transform-keys transforms/->kebab-case-keyword))]]]} + + "connector.dAppChainIdSwitched" + {:fx [[:dispatch + [:browser.rpc/on-chain-switched-signal + (->> event-js + transforms/js->clj + (cske/transform-keys transforms/->kebab-case-keyword))]]]} + + "connector.dAppPermissionRevoked" + {:fx [[:dispatch + [:browser.rpc/on-permission-revoked-signal + (->> event-js + transforms/js->clj + (cske/transform-keys transforms/->kebab-case-keyword))]]]} + + "connector.sendTransaction" + {:fx [[:dispatch + [:browser.rpc/on-transaction-signal + (->> event-js + transforms/js->clj + (cske/transform-keys transforms/->kebab-case-keyword))]]]} + (log/debug "Event " type " not handled")))) diff --git a/src/status_im/contexts/browser/api.cljs b/src/status_im/contexts/browser/api.cljs new file mode 100644 index 00000000000..cddcda6b567 --- /dev/null +++ b/src/status_im/contexts/browser/api.cljs @@ -0,0 +1,65 @@ +(ns status-im.contexts.browser.api + (:require [camel-snake-kebab.extras :as cske] + [cljs.pprint :as pprint] + [promesa.core :as promesa] + [status-im.common.json-rpc.events :as rpc] + [utils.transforms :as transforms])) + +(defn- process-dapp-permissions + [permissions] + (->> permissions + (cske/transform-keys transforms/->kebab-case-keyword) + (into {} (map (juxt :url identity))))) + +(defn get-dapp-permissions + [] + (-> (rpc/call-async :connector_getPermittedDAppsList false) + (promesa/then process-dapp-permissions))) + +(defn call-connector-rpc + [json-rpc dapp] + (let [arg (-> {:params [] + :jsonrpc "2.0"} + (merge json-rpc) + (merge dapp) + (dissoc :topic))] + (rpc/call-async :connector_callRPC false (transforms/clj->json arg)))) + +(defn approve-accounts-request + [response] + (->> response + clj->js + (rpc/call-async :connector_requestAccountsAccepted false))) + +(defn reject-accounts-request + [response] + (->> response + clj->js + (rpc/call-async :connector_requestAccountsRejected false))) + +(defn approve-transaction + [response] + (->> response + clj->js + (rpc/call-async :connector_sendTransactionAccepted false))) + +(defn reject-transaction + [response] + (->> response + clj->js + (rpc/call-async :connector_sendTransactionRejected false))) + +(defn get-dapp-browsers + [] + (-> (rpc/call-async :wakuext_getBrowsers false) + (promesa/then process-dapp-permissions))) + +(defn get-dapp-bookmarks + [] + (-> (rpc/call-async :wakuext_getBookmarks false) + (promesa/then process-dapp-permissions))) + +(defn save-browser + [browser] + (-> (rpc/call-async :wakuext_addBrowser browser false) + (promesa/then process-dapp-permissions))) diff --git a/src/status_im/contexts/browser/components/dapp_bar.cljs b/src/status_im/contexts/browser/components/dapp_bar.cljs new file mode 100644 index 00000000000..a20a4c77a72 --- /dev/null +++ b/src/status_im/contexts/browser/components/dapp_bar.cljs @@ -0,0 +1,46 @@ +(ns status-im.contexts.browser.components.dapp-bar + (:require [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn])) + +(defn container + [& children] + (into [rn/view + {:style {:padding 8 + :background-color colors/neutral-80 + :flex-direction :row + :border-radius 20 + :height 44 + :margin-horizontal 12 + :padding-horizontal 8 + :flex 1 + :align-items :center + :justify-content :space-between}}] + children)) + +(defn title + [{:keys [text container-style]}] + [rn/view + {:style (merge {:padding-horizontal 2 + :flex 1 + :align-items :center + :justify-content :center} + container-style)} + [quo/text + {:size :paragraph-1 + :number-of-lines 1 + :weight :bold + :style {:color colors/white-opa-80}} + text]]) + +(defn info-button + [] + [rn/view + {:style + {:width 24 + :height 24 + :align-items :center + :justify-content :center}} + [quo/icon :i/info + {:size 20 + :color colors/white-opa-80}]]) diff --git a/src/status_im/contexts/browser/components/dapp_icon.cljs b/src/status_im/contexts/browser/components/dapp_icon.cljs new file mode 100644 index 00000000000..d46ff4cdba7 --- /dev/null +++ b/src/status_im/contexts/browser/components/dapp_icon.cljs @@ -0,0 +1,27 @@ +(ns status-im.contexts.browser.components.dapp-icon + (:require [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.fast-image :as fast-image])) + +(defn view + [{:keys [dapp size background-color]}] + (let [icon (:icon dapp) + image (:logo-url dapp)] + (when (or icon image) + [rn/view + {:padding 4 + :border-radius 20 + :background-color background-color + :overflow :hidden} + (cond + icon + [rn/view {:style {:transform [{:scale 0.8}]}} + [quo/icon (:icon dapp) + {:size size + :color colors/white-opa-80}]] + + image + [fast-image/fast-image + {:source (:logo-url dapp) + :style {:width size :height size :border-radius 20}}])]))) diff --git a/src/status_im/contexts/browser/components/request_accounts_sheet.cljs b/src/status_im/contexts/browser/components/request_accounts_sheet.cljs new file mode 100644 index 00000000000..d9c2ffba8dd --- /dev/null +++ b/src/status_im/contexts/browser/components/request_accounts_sheet.cljs @@ -0,0 +1,63 @@ +(ns status-im.contexts.browser.components.request-accounts-sheet + (:require [quo.core :as quo] + [react-native.core :as rn] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn on-approve + [request address] + (rf/dispatch [:browser.rpc/approve-request-accounts request address]) + (rf/dispatch [:hide-bottom-sheet])) + +(defn on-reject + [request] + (rf/dispatch [:browser.rpc/reject-request-accounts request]) + (rf/dispatch [:hide-bottom-sheet])) + +(defn view + [{:keys [request]}] + (let [{:keys [icon-url name url]} request + accounts (rf/sub [:wallet/operable-accounts]) + addresses (map :address accounts) + [selected-address set-selected-address] (rn/use-state (first addresses))] + (println :selected selected-address) + [rn/view {:style {:padding-top 20}} + [rn/view {:style {:margin-bottom 20}} + [rn/view + {:style {:margin-bottom 12 + :padding-horizontal 20}} + [quo/user-avatar + {:profile-picture icon-url + :size :big + :full-name name}]] + [quo/page-top + {:title name + :description :context-tag + :context-tag {:type :icon + :size 32 + :icon :i/link + :context url}}]] + [rn/view {:style {:padding-horizontal 20}} + [quo/text + {:size :heading-2 + :weight :semi-bold + :accessibility-label "select-account-title"} + (i18n/label :t/select-account)] + [rn/view {:style {:margin-top 12 :margin-bottom 20}} + (for [{:keys [address] :as account} accounts] + ^{:key (str address)} + [quo/account-item + {:type :default + :state (if (= address selected-address) + :selected + :default) + :account-props account + :on-press (fn [] + (set-selected-address (:address account)))}])]] + [quo/bottom-actions + {:actions :two-actions + :button-one-label "Approve" + :button-one-props {:on-press #(on-approve request selected-address)} + :button-two-label "Reject" + :button-two-props {:type :outline + :on-press #(on-reject request)}}]])) diff --git a/src/status_im/contexts/browser/components/request_transaction.cljs b/src/status_im/contexts/browser/components/request_transaction.cljs new file mode 100644 index 00000000000..6efda3c16f7 --- /dev/null +++ b/src/status_im/contexts/browser/components/request_transaction.cljs @@ -0,0 +1,54 @@ +(ns status-im.contexts.browser.components.request-transaction + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.raw-data-block.view :as raw-data-block] + [utils.re-frame :as rf] + [utils.transforms :as transforms])) + +(defn on-approve + [{:keys [tx-args] :as request}] + ;;(rf/dispatch [:browser.rpc/approve-transaction request tx-hash]) + (rf/dispatch [:browser.rpc/reject-transaction request]) + (rf/dispatch [:hide-bottom-sheet])) + +(defn on-reject + [request] + (rf/dispatch [:browser.rpc/reject-transaction request]) + (rf/dispatch [:hide-bottom-sheet])) + +(defn view + [{:keys [request]}] + (let [{:keys [icon-url name url tx-args]} request] + [rn/view {:style {:padding-top 20}} + [rn/view {:style {:margin-bottom 20}} + [rn/view + {:style {:margin-bottom 12 + :padding-horizontal 20}} + [quo/user-avatar + {:profile-picture icon-url + :size :big + :full-name name}]] + [quo/page-top + {:title name + :description :context-tag + :context-tag {:type :icon + :size 32 + :icon :i/link + :context url}}]] + [rn/view {:style {:padding-horizontal 20}} + [quo/text + {:size :heading-2 + :weight :semi-bold + :accessibility-label "select-account-title"} + "Sign transaction"] + [raw-data-block/view + (-> tx-args + transforms/json->clj + (transforms/clj->pretty-json 2))]] + [quo/bottom-actions + {:actions :two-actions + :button-one-label "Approve" + :button-one-props {:on-press #(on-approve request)} + :button-two-label "Reject" + :button-two-props {:type :outline + :on-press #(on-reject request)}}]])) diff --git a/src/status_im/contexts/browser/constants.cljs b/src/status_im/contexts/browser/constants.cljs new file mode 100644 index 00000000000..0f7f2155c06 --- /dev/null +++ b/src/status_im/contexts/browser/constants.cljs @@ -0,0 +1,8 @@ +(ns status-im.contexts.browser.constants + (:require [react-native.core :as rn])) + +(def footer-height 80) +(def browser-width (-> (rn/get-window) :width)) +(def browser-height "100%") + +(def default-chain-id 1) diff --git a/src/status_im/contexts/browser/core.cljs b/src/status_im/contexts/browser/core.cljs new file mode 100644 index 00000000000..79363d27dcd --- /dev/null +++ b/src/status_im/contexts/browser/core.cljs @@ -0,0 +1,12 @@ +(ns status-im.contexts.browser.core + (:require [status-im.contexts.browser.constants :as browser.constants])) + +(defn freeze-tab? + [tab-id focused-tab-id] + (-> #{(dec focused-tab-id) focused-tab-id (inc focused-tab-id)} + (contains? tab-id) + not)) + +(defn tab-position + [tab-idx] + (* tab-idx browser.constants/browser-width)) diff --git a/src/status_im/contexts/browser/db.cljs b/src/status_im/contexts/browser/db.cljs new file mode 100644 index 00000000000..22920a9dac3 --- /dev/null +++ b/src/status_im/contexts/browser/db.cljs @@ -0,0 +1,50 @@ +(ns status-im.contexts.browser.db + (:require [status-im.contexts.browser.constants :as browser.constants])) + +(defn get-tab-ids + [db] + (get db :browser/tab-ids)) + +(defn get-tabs-by-id + [db] + (get db :browser/tabs-by-id)) + +(defn get-dapps + [db] + (get db :browser/dapps)) + +(defn get-dapp-by-id + [db dapp-id] + (-> db get-dapps (get dapp-id))) + +(defn get-tab-id-by-index + [db idx] + (-> db get-tab-ids (nth idx 0))) + +(defn get-tab + [db tab-id] + (-> db get-tabs-by-id (get tab-id))) + +(defn get-tab-url + [db tab-id] + (-> db (get-tab tab-id) :url)) + +(defn get-dapp-id + [db tab-id] + (-> db (get-tab tab-id) :dapp-id)) + +(defn get-tab-dapp + [db tab-id] + (->> (get-dapp-id db tab-id) + (get-dapp-by-id db))) + +(defn get-dapp-permissions + [db] + (get db :browser/permissions)) + +(defn get-dapp-chain-id + [db dapp-url] + (-> db + get-dapp-permissions + (get dapp-url) + (get :chain-id browser.constants/default-chain-id))) diff --git a/src/status_im/contexts/browser/events/core.cljs b/src/status_im/contexts/browser/events/core.cljs new file mode 100644 index 00000000000..c91a7e5b885 --- /dev/null +++ b/src/status_im/contexts/browser/events/core.cljs @@ -0,0 +1,101 @@ +(ns status-im.contexts.browser.events.core + (:require [cljs.pprint :as pprint] + [re-frame.core :as rf] + [status-im.contexts.browser.db :as browser.db] + status-im.contexts.browser.events.effects + status-im.contexts.browser.events.rpc + status-im.contexts.browser.events.screenshots + [status-im.contexts.browser.messages :as messages] + [status-im.contexts.browser.native-dapps :as native-dapps])) + +(rf/reg-event-fx :browser/set-tab-ref + (fn [{:keys [db]} [tab-id ref]] + {:db (assoc-in db [:browser/tabs-by-id tab-id :ref] ref)})) + +(rf/reg-event-fx :browser/add-tab + (fn [{:keys [db]} [url tab-type]] + (let [tab-id (random-uuid)] + {:db (-> db + (assoc-in [:browser/tabs-by-id tab-id] + {:type tab-type + :url url + :dapp-id url + :id tab-id}) + (update :browser/tab-ids conj tab-id))}))) + +(rf/reg-event-fx :browser/focus-tab-by-idx + (fn [{:keys [db]} [tab-idx]] + (let [tab-id (browser.db/get-tab-id-by-index db tab-idx)] + {:fx [[:dispatch [:browser/focus-tab tab-id]]]}))) + +(rf/reg-event-fx :browser/focus-tab + (fn [{:keys [db]} [tab-id]] + (let [tab-ids (get db :browser/tab-ids) + valid-tab-id? (-> tab-ids set (contains? tab-id))] + (when valid-tab-id? + {:db (assoc db :browser/focused-tab-id tab-id)})))) + +(rf/reg-event-fx :browser/save-native-dapp + (fn [_ [dapp-id]] + {:fx [[:dispatch [:browser/save-dapp dapp-id (get native-dapps/metadata dapp-id)]]]})) + +(rf/reg-event-fx :browser/init-native-dapps + (fn [_] + {:fx [[:dispatch [:browser/save-native-dapp "wallet.status"]] + [:dispatch [:browser/save-native-dapp "messages.status"]] + [:dispatch [:browser/save-native-dapp "communities.status"]] + [:dispatch [:browser/add-tab "wallet.status" :tab/native]] + [:dispatch [:browser/add-tab "messages.status" :tab/native]] + [:dispatch [:browser/add-tab "communities.status" :tab/native]]]})) + +(rf/reg-event-fx :browser/init + (fn [{:keys [db]}] + {:db (assoc db :browser/mode :browser-mode/browser) + :fx [[:dispatch [:browser/init-native-dapps]] + [:dispatch [:browser/init-tabs]] + [:dispatch [:browser.rpc/get-permissions]]]})) + +(rf/reg-event-fx :browser/init-tabs + (fn [{:keys [db]}] + {:db (-> db + (assoc :browser/tabs-by-id {} + :browser/tab-ids [])) + :fx [[:dispatch [:browser/add-tab "https://pancakeswap.finance/swap" :tab/web]] + [:dispatch [:browser/add-tab "https://app.uniswap.org" :tab/web]]]})) + +(rf/reg-event-fx :browser/on-message + (fn [{:keys [_]} [tab-id js-event]] + (let [event (messages/parse-native-event js-event) + event-topic (messages/event-topic event)] + {:fx [(condp = event-topic + "website-metadata" [:dispatch [:browser/on-website-metadata tab-id event]] + "rpc" [:dispatch [:browser.rpc/on-event tab-id event]] + (do (println :unhandled-event-topic event-topic) + (pprint/pprint event)))]}))) + +(rf/reg-event-fx :browser/on-website-metadata + (fn [{:keys [db]} [tab-id event]] + (let [{:keys [origin-url logo-url page-title]} (messages/get-website-metadata event) + dapp-metadata {:logo-url logo-url + :origin-url origin-url + :title page-title}] + {:db (update-in db + [:browser/tabs-by-id tab-id] + assoc + :dapp-id + origin-url) + :fx [[:dispatch [:browser/save-dapp origin-url dapp-metadata]]]}))) + +(rf/reg-event-fx :browser/save-dapp + (fn [{:keys [db]} [dapp-id dapp-metadata]] + (when-not (contains? (:browser/dapps db) dapp-id) + ;;TODO: persist dapps + {:db (assoc-in db [:browser/dapps dapp-id] dapp-metadata)}))) + +(rf/reg-event-fx :browser/show-tabs + (fn [{:keys [db]}] + {:db (assoc db :browser/mode :browser-mode/tabs)})) + +(rf/reg-event-fx :browser/show-browser + (fn [{:keys [db]}] + {:db (assoc db :browser/mode :browser-mode/browser)})) diff --git a/src/status_im/contexts/browser/events/effects.cljs b/src/status_im/contexts/browser/events/effects.cljs new file mode 100644 index 00000000000..e8a687cbfbe --- /dev/null +++ b/src/status_im/contexts/browser/events/effects.cljs @@ -0,0 +1,8 @@ +(ns status-im.contexts.browser.events.effects + (:require [re-frame.core :as rf] + [react-native.view-shot :as view-shot] + [react-native.webview :as webview])) + +(rf/reg-fx :fx.browser/send-message + (fn [[webview-ref message]] + (webview/post-message webview-ref message))) diff --git a/src/status_im/contexts/browser/events/rpc.cljs b/src/status_im/contexts/browser/events/rpc.cljs new file mode 100644 index 00000000000..4f7ab6e04c7 --- /dev/null +++ b/src/status_im/contexts/browser/events/rpc.cljs @@ -0,0 +1,185 @@ +(ns status-im.contexts.browser.events.rpc + (:require [cljs.pprint :as pprint] + [re-frame.core :as rf] + [status-im.contexts.browser.api :as browser.api] + [status-im.contexts.browser.components.request-accounts-sheet :as request-accounts-sheet] + [status-im.contexts.browser.db :as browser.db] + [status-im.contexts.wallet.networks.db :as networks.db] + [taoensso.timbre :as log] + [utils.hex :as hex])) + +(rf/reg-event-fx :browser.rpc/get-permissions + (fn [_] + {:fx [[:fx.promise + {:promise browser.api/get-dapp-permissions + :on-success [:browser/store-permissions] + :on-error #(log/error "Failed to get dapp permissions" {:error %})}]]})) + +(rf/reg-event-fx :browser/store-permissions + (fn [{:keys [db]} [permissions]] + {:db (assoc db :browser/permissions permissions)})) + +(rf/reg-event-fx :browser.rpc/process-rpc + (fn [{:keys [db]} [tab-id rpc-event]] + (let [tab (-> db (browser.db/get-tab tab-id)) + connector-dapp {:url (:dapp-id tab) + :name (-> tab :metadata :title (or (:dapp-id tab))) + :iconUrl (-> tab :metadata :logo-url) + :chainId (browser.db/get-dapp-chain-id db (:dapp-id tab))}] + {:fx [[:fx.promise + {:promise #(browser.api/call-connector-rpc rpc-event connector-dapp) + :on-success [:browser.rpc/send tab-id rpc-event] + :on-error [:browser.rpc/send-error tab-id rpc-event]}]]}))) + +(rf/reg-event-fx :browser.rpc/send + (fn [{:keys [db]} [tab-id rpc-event message]] + (let [rpc-id (:id rpc-event) + webview-ref (get-in db [:browser/tabs-by-id tab-id :ref])] + {:fx [[:fx.browser/send-message + [webview-ref + {:id rpc-id + :jsonrpc "2.0" + :type "rpcResponse" + :result message}]] + [:dispatch [:browser.rpc/call-next-in-queue]]]}))) + +(rf/reg-event-fx :browser.rpc/send-error + (fn [{:keys [db]} [tab-id rpc-event error]] + (let [rpc-id (:id rpc-event) + webview-ref (get-in db [:browser/tabs-by-id tab-id :ref])] + {:fx [[:fx.browser/send-message + [webview-ref + {:id rpc-id + :jsonrpc "2.0" + :type "rpcResponse" + :error error}]] + [:dispatch [:browser.rpc/call-next-in-queue]]]}))) + +(rf/reg-event-fx :browser.rpc/on-event + (fn [{:keys [db]} [tab-id event]] + (let [current-queue (get db :browser/rpc-queue []) + rpc-event (:data event) + new-queue (conj current-queue + {:tab-id tab-id + :event rpc-event})] + {:db (assoc db :browser/rpc-queue new-queue) + :fx [(when (empty? current-queue) + [:dispatch [:browser.rpc/process-rpc tab-id rpc-event]])]}))) + +(rf/reg-event-fx :browser.rpc/call-next-in-queue + (fn [{:keys [db]}] + (let [current-queue (get db :browser/rpc-queue []) + remaining-queue (-> current-queue rest vec) + {:keys [tab-id event]} (first remaining-queue)] + {:db (assoc db :browser/rpc-queue remaining-queue) + :fx [(when event + [:dispatch [:browser.rpc/process-rpc tab-id event]])]}))) + +(rf/reg-event-fx :browser.rpc/approve-request-accounts + (fn [{:keys [db]} [request address]] + (let [response {:requestId (:request-id request) + :chainId (browser.db/get-dapp-chain-id db (:url request)) + :account address}] + {:fx [[:fx.promise + {:promise #(browser.api/approve-accounts-request response) + :on-error #(log/error "Failed to approve" + {:error % + :response response})}]]}))) + +(rf/reg-event-fx :browser.rpc/reject-request-accounts + (fn [_ [request]] + (let [response {:requestId (:request-id request)}] + {:fx [[:fx.promise + {:promise #(browser.api/reject-accounts-request response) + :on-error #(log/error "Failed to reject" + {:error % + :response response})}]]}))) + +(rf/reg-event-fx :browser.rpc/on-permission-granted-signal + (fn [{:keys [db]} [connector-dapp]] + (let [{:keys [url name] :as dapp-permission} (-> connector-dapp + (assoc :chain-id (-> connector-dapp :chains first)) + (dissoc :chains))] + (log/info "dApp permission granted") + {:db (assoc-in db [:browser/permissions url] dapp-permission) + :fx [[:dispatch + [:toasts/upsert + {:type :positive + :text (str "Connected to " name)}]]]}))) + +(rf/reg-event-fx :browser.rpc/on-permission-revoked-signal + (fn [{:keys [db]} [{:keys [url name]}]] + (log/info "dApp permission revoked") + {:db (update-in db [:browser/permissions] dissoc url) + :fx [[:dispatch + [:toasts/upsert + {:type :positive + :text (str "Disconnected from " name)}]]]})) + +(rf/reg-event-fx :browser.rpc/show-approval-sheet + (fn [_ [{:keys [content]}]] + {:fx [[:dispatch + [:show-bottom-sheet + {:hide-handle? true + :drag-content? false + :hide-on-background-press? false + :content content}]]]})) + +(rf/reg-event-fx :browser.rpc/on-request-accounts-signal + (fn [_ [request]] + {:fx [[:dispatch + [:browser.rpc/show-approval-sheet + {:content (fn [] + [request-accounts-sheet/view {:request request}])}]]]})) + +(rf/reg-event-fx :browser.rpc/on-transaction-signal + (fn [_ [request]] + {:fx [[:dispatch + [:browser.rpc/show-approval-sheet + {:content (fn [] + [request-accounts-sheet/view {:request request}])}]]]})) + +;; (rf/reg-event-fx :browser.rpc/sign-transaction +;; (fn [{:keys [db]} [request]] +;; {:fx [[:dispatch +;; [:standard-auth/authorize-and-sign +;; {:sign-payload (:tx-args request) +;; :theme :dark +;; :blur? false +;; :on-sign-success (fn [signatures] +;; (let [ (-> signatures +;; first +;; :signature +;; hex/prefix-hex)])) +;; :on-sign-error identity +;; :auth-button-label "Sign transaction"}]]]})) + +(rf/reg-event-fx :browser.rpc/approve-transaction + (fn [_ [request tx-hash]] + (let [response {:requestId (:request-id request) + :hash tx-hash}] + {:fx [[:fx.promise + {:promise #(browser.api/approve-accounts-request response) + :on-error #(log/error "Failed to approve" + {:error % + :response response})}]]}))) + +(rf/reg-event-fx :browser.rpc/reject-transaction + (fn [_ [request]] + (let [response {:requestId (:request-id request)}] + {:fx [[:fx.promise + {:promise #(browser.api/reject-accounts-request response) + :on-error #(log/error "Failed to reject" + {:error % + :response response})}]]}))) + +(rf/reg-event-fx :browser.rpc/on-chain-switched-signal + (fn [{:keys [db]} [{:keys [chain-id url]}]] + (let [{:keys [title]} (browser.db/get-dapp-by-id db url) + new-chain-id (hex/hex-to-number chain-id) + network-name (networks.db/get-network-name db new-chain-id)] + {:db (assoc-in db [:browser/permissions url :chain-id] new-chain-id) + :fx [[:dispatch + [:toasts/upsert + {:type :positive + :text (str title " switched the network to " network-name)}]]]}))) diff --git a/src/status_im/contexts/browser/events/screenshots.cljs b/src/status_im/contexts/browser/events/screenshots.cljs new file mode 100644 index 00000000000..a67d292202d --- /dev/null +++ b/src/status_im/contexts/browser/events/screenshots.cljs @@ -0,0 +1,33 @@ +(ns status-im.contexts.browser.events.screenshots + (:require [re-frame.core :as rf] + [react-native.core :as rn] + [react-native.view-shot :as view-shot] + [taoensso.timbre :as log])) + +(rf/reg-event-fx :browser.screenshots/capture + (fn [{:keys [db]} [tab-id]] + (let [ref (get-in db [:browser/screenshots tab-id :ref])] + (when ref + {:fx [[:fx.promise + {:promise #(view-shot/capture ref) + :on-success [:browser.screenshots/add-url tab-id] + :on-error #(log/error "Failed to capture tab screenshot" {:error %})}]]})))) + +(rf/reg-event-fx :browser.screenshots/add-url + (fn [{:keys [db]} [tab-id url]] + {:db (assoc-in db [:browser/screenshots tab-id :url] url) + :fx [#_[:dispatch + [:show-bottom-sheet + {:content (fn [] [rn/view {:style {:flex 1 :align-items :center :justify-content :center}} + [rn/image + {:source {:uri url} + :style {:height 300 + :width 300 + :border-width 2 + :border-color :red + :aspect-ratio 0.5 + :transform [{:scale 1}]}}]])}]]]})) + +(rf/reg-event-fx :browser.screenshots/add-ref + (fn [{:keys [db]} [tab-id ref]] + {:db (assoc-in db [:browser/screenshots tab-id :ref] ref)})) diff --git a/src/status_im/contexts/browser/footer/view.cljs b/src/status_im/contexts/browser/footer/view.cljs new file mode 100644 index 00000000000..85e04dbe6c1 --- /dev/null +++ b/src/status_im/contexts/browser/footer/view.cljs @@ -0,0 +1,92 @@ +(ns status-im.contexts.browser.footer.view + (:require [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.reanimated :as reanimated] + [status-im.contexts.browser.components.dapp-bar :as dapp-bar] + [status-im.contexts.browser.components.dapp-icon :as dapp-icon] + [status-im.contexts.browser.constants :as browser.constants] + [status-im.contexts.profile.utils :as profile.utils] + [utils.re-frame :as rf])) + +(defn dapp-bar + [] + (let [tab-id (rf/sub [:browser/focused-tab-id]) + dapp (rf/sub [:browser/dapp-for-tab tab-id])] + [dapp-bar/container + [dapp-icon/view + {:dapp dapp + :size 24}] + [dapp-bar/title {:text (:title dapp)}] + [dapp-bar/info-button]])) + +(defn profile-btn + [] + (let [{:keys [public-key] :as profile} (rf/sub [:profile/profile-with-image]) + online? (rf/sub [:visibility-status-updates/online? + public-key]) + customization-color (rf/sub [:profile/customization-color]) + avatar-props {:online? online? + :full-name (profile.utils/displayed-name profile) + :profile-picture (profile.utils/photo profile)} + on-press #(rf/dispatch [:open-modal :screen/settings])] + [rn/pressable + {:on-press on-press + :style {:padding 4 + :background-color colors/neutral-80 + :border-radius 40} + :accessibility-label :open-profile} + [quo/user-avatar + (merge {:status-indicator? true + :customization-color customization-color + :size :small} + avatar-props)]])) + +(defn on-tabs-press + [browser-mode] + (condp = browser-mode + :browser-mode/browser (rf/dispatch [:browser/show-tabs]) + :browser-mode/tabs (rf/dispatch [:browser/show-browser]) + nil)) + +(defn tabs-btn-icon + [browser-mode] + (condp = browser-mode + :browser-mode/tabs :i/browser + :browser-mode/browser :i/tabs + :i/tabs)) + +(defn tabs-btn + [] + (let [browser-mode (rf/sub [:browser/mode])] + [rn/pressable + {:style {:flex 1 + :align-items :center + :justify-content :center} + :on-press #(on-tabs-press browser-mode)} + [rn/view {:style {:transform [{:scale 1.2}]}} + [quo/icon (tabs-btn-icon browser-mode) {:size 20 :color colors/white}]]])) + +(defn view + [] + [reanimated/view + {:style {:width browser.constants/browser-width + :background-color colors/neutral-100 + :align-items :center + :justify-content :space-between + :flex-direction :row + :padding-horizontal 20 + :height browser.constants/footer-height}} + [rn/view + {:style {:align-items :center + :justify-content :center + :margin-right 8}} + [profile-btn]] + [dapp-bar] + [rn/view + {:style {:width 44 + :height 44 + :margin-left 8 + :background-color colors/neutral-80 + :border-radius 12}} + [tabs-btn]]]) diff --git a/src/status_im/contexts/browser/hooks.cljs b/src/status_im/contexts/browser/hooks.cljs new file mode 100644 index 00000000000..b4197b77250 --- /dev/null +++ b/src/status_im/contexts/browser/hooks.cljs @@ -0,0 +1,91 @@ +(ns status-im.contexts.browser.hooks + (:require [oops.core :as oops] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.reanimated :as reanimated] + [react-native.view-shot :as view-shot] + [status-im.contexts.browser.core :as browser] + [utils.re-frame :as rf] + [utils.worklets.browser :as worklets.browser])) + +(defn animate-with-spring + [animation-val to-val] + (reanimated/animate-shared-value-with-spring animation-val + to-val + {:mass 1 + :damping 100 + :stiffness 400})) + +(def drag-threshold 100) +(defn- make-drag-gesture + [{:keys [enabled? can-move-left? can-move-right? x-translation-value focused-tab-idx on-tab-change]}] + (let [initial-x-val (atom 0)] + (-> + (gesture/gesture-pan) + (gesture/enabled enabled?) + (gesture/on-begin (fn [_] + (reset! initial-x-val (browser/tab-position focused-tab-idx)))) + (gesture/on-update (fn [event] + (let [x-translation (oops/oget event "translationX") + new-val (- @initial-x-val x-translation)] + (reanimated/set-shared-value x-translation-value new-val)))) + (gesture/on-end + (fn [event] + (let [x-translation (oops/oget event "translationX") + drag-right? (>= x-translation drag-threshold) + drag-left? (<= x-translation (- drag-threshold))] + (cond + (and drag-right? can-move-left?) + (let [tab-idx (dec focused-tab-idx)] + (animate-with-spring x-translation-value (browser/tab-position tab-idx)) + (on-tab-change tab-idx)) + + (and drag-left? can-move-right?) + (let [tab-idx (inc focused-tab-idx)] + (animate-with-spring x-translation-value (browser/tab-position tab-idx)) + (on-tab-change tab-idx)) + + :else + (animate-with-spring x-translation-value @initial-x-val)))))))) + +(defn use-swipe-gesture + [scroll-ref] + (let [focused-tab-idx (rf/sub [:browser/focused-tab-idx]) + browser-mode (rf/sub [:browser/mode]) + gestures-enabled? (= browser-mode :browser-mode/browser) + x-translation-value (reanimated/use-shared-value + (or (browser/tab-position focused-tab-idx) 0)) + can-focus-next? (rf/sub [:browser/can-focus-next?]) + can-focus-prev? (rf/sub [:browser/can-focus-prev?])] + + (rn/use-effect + (fn [] + (reanimated/set-shared-value x-translation-value (browser/tab-position focused-tab-idx))) + [focused-tab-idx]) + + (worklets.browser/use-scroll-tab {:animated-ref scroll-ref + :x-translation x-translation-value + :animate true}) + + {:x-translation-value x-translation-value + :gesture-prop (make-drag-gesture + {:enabled? gestures-enabled? + :x-translation-value x-translation-value + :can-move-left? can-focus-prev? + :can-move-right? can-focus-next? + :focused-tab-idx focused-tab-idx + :on-tab-change (fn [tab-idx] + (js/setTimeout #(rf/dispatch-sync + [:browser/focus-tab-by-idx tab-idx]) + 200))})})) + +(defn use-screenshot-tab + [tab-id] + (let [options {:fileName (str "tab-screenshot-" tab-id) + :format :jpg + :quality 0.9}] + {:options options + :capture (fn [] + (rf/dispatch [:browser.screenshots/capture tab-id])) + :ref (fn [ref] + (rf/dispatch [:browser.screenshots/add-ref tab-id ref]))})) diff --git a/src/status_im/contexts/browser/js_scripts/core.cljs b/src/status_im/contexts/browser/js_scripts/core.cljs new file mode 100644 index 00000000000..8f4cc07ae20 --- /dev/null +++ b/src/status_im/contexts/browser/js_scripts/core.cljs @@ -0,0 +1,21 @@ +(ns status-im.contexts.browser.js-scripts.core + (:require [shadow.resource :as rc] + [utils.transforms :as transforms])) + +(defn add-global-var + [script var-name value] + (-> script + (str "window.__" var-name "__ = " (transforms/clj->json value) ";"))) + +(defn add-script + [script more-script] + (-> script + (str more-script))) + +;; NOTE: using `rc/inline` instead of `slurp`, so that changes to the js files trigger a recompilation +;; and are included in the bundle. With `slurp`, you'd have to either re-run `shadow-cljs` or +;; re-evaluate the slurp in the REPL. +(def web3-provider (rc/inline "./web3_provider.js")) +(def freeze-website (rc/inline "./freeze_website.js")) +(def unfreeze-website (rc/inline "./unfreeze_website.js")) +(def website-metadata (rc/inline "./website_metadata.js")) diff --git a/src/status_im/contexts/browser/js_scripts/freeze_website.js b/src/status_im/contexts/browser/js_scripts/freeze_website.js new file mode 100644 index 00000000000..2b278f7fd5d --- /dev/null +++ b/src/status_im/contexts/browser/js_scripts/freeze_website.js @@ -0,0 +1,25 @@ +(function () { + // pause media elements + var mediaElements = document.querySelectorAll('video:not([paused]), audio:not([paused])'); + mediaElements.forEach(function (element) { + element.setAttribute('data-frozen-playback-state', element.paused ? 'paused' : 'playing'); + element.setAttribute('data-frozen', 'true'); + element.pause(); + }); + + // Suspend expensive animations and transitions + var animatedElements = document.querySelectorAll('*[style*="animation"], *[style*="transition"]'); + animatedElements.forEach(function (element) { + element.setAttribute('data-frozen-animation-play-state', element.style.animationPlayState); + element.setAttribute('data-frozen-transition-property', element.style.transitionProperty); + element.style.animationPlayState = 'paused'; + element.style.transitionProperty = 'none'; + }); + + // Suspend keyframe animations + var keyframeAnimatedElements = document.querySelectorAll('*[style*="animation-name"]'); + keyframeAnimatedElements.forEach(function (element) { + element.setAttribute('data-frozen-animation-name', element.style.animationName); + element.style.animationName = 'none'; + }); +})(); diff --git a/src/status_im/contexts/browser/js_scripts/unfreeze_website.js b/src/status_im/contexts/browser/js_scripts/unfreeze_website.js new file mode 100644 index 00000000000..0a5913393d5 --- /dev/null +++ b/src/status_im/contexts/browser/js_scripts/unfreeze_website.js @@ -0,0 +1,27 @@ +(function () { + // resume media elements + var pausedMediaElements = document.querySelectorAll('video[data-frozen="true"], audio[data-frozen="true"]'); + pausedMediaElements.forEach(function (element) { + if (element.getAttribute('data-frozen-playback-state') === 'playing') { + element.play(); + } + element.removeAttribute('data-frozen'); + element.removeAttribute('data-frozen-playback-state'); + }); + + // Resume animations and transitions + var animatedElements = document.querySelectorAll('*[style*="animation"], *[style*="transition"]'); + animatedElements.forEach(function (element) { + element.style.animationPlayState = element.getAttribute('data-frozen-animation-play-state') || 'running'; + element.style.transitionProperty = element.getAttribute('data-frozen-transition-property') || ''; + element.removeAttribute('data-frozen-animation-play-state'); + element.removeAttribute('data-frozen-transition-property'); + }); + + // Resume keyframe animations + var keyframeAnimatedElements = document.querySelectorAll('*[style*="animation-name"]'); + keyframeAnimatedElements.forEach(function (element) { + element.style.animationName = element.getAttribute('data-frozen-animation-name') || ''; + element.removeAttribute('data-frozen-animation-name'); + }); +})(); diff --git a/src/status_im/contexts/browser/js_scripts/web3_provider.js b/src/status_im/contexts/browser/js_scripts/web3_provider.js new file mode 100644 index 00000000000..36b0cf1a656 --- /dev/null +++ b/src/status_im/contexts/browser/js_scripts/web3_provider.js @@ -0,0 +1,97 @@ +console.log('TESTING INJECTION'); + +(function () { + // Internal storage for pending calls + const _callbacks = {}; + let _nextId = 1; + + // Minimal EventEmitter for provider.on(...) + const _listeners = {}; + + // 1️⃣ Define our fake provider + const fakeProvider = { + // EIP-1193 request() + request({ method, params }) { + return new Promise((resolve, reject) => { + const id = _nextId++; + _callbacks[id] = { resolve, reject }; + // send to RN side + window.ReactNativeWebView.postMessage(JSON.stringify({ topic: 'rpc', id, method, params })); + }); + }, + + // EIP-1193 event subscription + on(eventName, handler) { + _listeners[eventName] = _listeners[eventName] || []; + _listeners[eventName].push(handler); + }, + }; + + // window.ReactNativeWebView.onMessage = function (ev) { + // console.log('MESSAGE INCOMING', ev); + // let msg; + // try { + // msg = JSON.parse(ev); + // } catch (e) { + // alert('ERROR parsing:', e, ev); + // return; + // } + + // console.log('message', msg); + // // RPC response + // if (msg.type === 'rpcResponse' && _callbacks[msg.id]) { + // const { resolve, reject } = _callbacks[msg.id]; + + // console.log('resolving rcpResponse', msg.id, _callbacks[msg.id]); + // delete _callbacks[msg.id]; + // msg.error ? reject(msg.error) : resolve(msg.result); + // } + + // // Emitted event from RN + // else if (msg.type === 'emitEvent' && _listeners[msg.event]) { + // _listeners[msg.event].forEach((fn) => fn(msg.data)); + // } + // }; + + //2️⃣ Listen for RN → WebView messages + document.addEventListener('message', (ev) => { + console.log('Message received from RN', ev.data); + + let msg; + try { + msg = JSON.parse(ev.data); + } catch (e) { + console.error('ERROR parsing:', e, ev); + return; + } + + // RPC response + if (msg.type === 'rpcResponse' && _callbacks[msg.id]) { + const { resolve, reject } = _callbacks[msg.id]; + delete _callbacks[msg.id]; + msg.error ? reject(msg.error) : resolve(msg.result); + } + + // Emitted event from RN + else if (msg.type === 'emitEvent' && _listeners[msg.event]) { + _listeners[msg.event].forEach((fn) => fn(msg.data)); + } + }); + + // 3️⃣ Expose to the page + window.__RN_WALLET_PROVIDER__ = fakeProvider; + window.ethereum = fakeProvider; // if Dapp expects `window.ethereum` + + // 4️⃣ (Optional) announce via EIP-6963 discovery + const info = { + uuid: window.__WALLET_UUID__, + name: window.__WALLET_NAME__, + icon: window.__WALLET_ICON__, + rdns: window.__WALLET_RDNS__, + }; + const detail = { info, provider: fakeProvider }; + window.dispatchEvent(new CustomEvent('eip6963:announceProvider', { detail })); + window.addEventListener('eip6963:requestProvider', () => + window.dispatchEvent(new CustomEvent('eip6963:announceProvider', { detail })), + ); +})(); diff --git a/src/status_im/contexts/browser/js_scripts/website_metadata.js b/src/status_im/contexts/browser/js_scripts/website_metadata.js new file mode 100644 index 00000000000..67ff9958198 --- /dev/null +++ b/src/status_im/contexts/browser/js_scripts/website_metadata.js @@ -0,0 +1,56 @@ +requestAnimationFrame(() => { + const icons = Array.from( + document.querySelectorAll( + "link[rel='apple-touch-icon'], link[rel='shortcut icon'], link[rel='icon'], link[rel='icon'][type='image/svg+xml']", + ), + ); + let highestResIcon = { href: undefined, size: 0 }; + + for (const icon of icons) { + const iconHref = icon.getAttribute('href'); + if (icon.type === 'image/svg+xml') { + highestResIcon = { href: iconHref, size: 1000 }; + break; + } else { + const sizeAttribute = icon.getAttribute('sizes'); + if (sizeAttribute) { + const size = Math.max(...sizeAttribute.split('x').map((num) => parseInt(num, 10))); + if (size > highestResIcon.size) { + highestResIcon = { href: iconHref, size: size }; + if (size >= 180) break; + } + } else if (icon.rel === 'apple-touch-icon') { + highestResIcon = { href: iconHref, size: 180 }; + } else if (iconHref && !highestResIcon.href) { + highestResIcon = { href: iconHref, size: 0 }; + } + } + } + + let logoUrl; + if (highestResIcon.href) { + const cleanOrigin = window.location.origin.endsWith('/') ? window.location.origin : window.location.origin + '/'; + let cleanHref = highestResIcon.href; + if (!(highestResIcon.href.startsWith('http:') || highestResIcon.href.startsWith('https:'))) { + cleanHref = cleanHref.startsWith('/') ? cleanHref.substring(1) : cleanHref; + logoUrl = cleanOrigin + cleanHref; + } else { + logoUrl = cleanHref; + } + } else { + logoUrl = undefined; + } + + const pageTitle = document.title || undefined; + + const websiteMetadata = { + topic: 'website-metadata', + payload: { + originUrl: window.location.origin, + logoUrl: logoUrl, + pageTitle: pageTitle, + }, + }; + window.ReactNativeWebView.postMessage(JSON.stringify(websiteMetadata)); + true; +}); diff --git a/src/status_im/contexts/browser/messages.cljs b/src/status_im/contexts/browser/messages.cljs new file mode 100644 index 00000000000..6d5068b2cc2 --- /dev/null +++ b/src/status_im/contexts/browser/messages.cljs @@ -0,0 +1,36 @@ +(ns status-im.contexts.browser.messages + (:require [camel-snake-kebab.extras :as cske] + [oops.core :as oops] + [utils.transforms :as transforms])) + +(defn- parse-message-data + [data] + (->> data + transforms/json->clj + (cske/transform-keys transforms/->kebab-case-keyword))) + +(defn parse-native-event + [js-event] + (let [event (->> (oops/oget js-event "nativeEvent") + transforms/js->clj + (cske/transform-keys transforms/->kebab-case-keyword))] + (update event :data parse-message-data))) + +(defn event-topic + [event] + (-> event :data :topic)) + +(defn event-payload + [event] + (-> event :data :payload)) + +(defn get-webview-event-data + [event] + (-> event + (select-keys [:url :can-go-forward :can-go-back]))) + +(defn get-website-metadata + [event] + (-> event + event-payload + (select-keys [:page-title :logo-url :origin-url]))) diff --git a/src/status_im/contexts/browser/native_dapps.cljs b/src/status_im/contexts/browser/native_dapps.cljs new file mode 100644 index 00000000000..b1cb5d790b8 --- /dev/null +++ b/src/status_im/contexts/browser/native_dapps.cljs @@ -0,0 +1,34 @@ +(ns status-im.contexts.browser.native-dapps + (:require + [quo.context :as quo.context] + [status-im.contexts.chat.home.view :as chat] + [status-im.contexts.communities.home.view :as communities] + [status-im.contexts.wallet.home.view :as wallet])) + +(defn- stack + [stack-id & children] + (let [theme (quo.context/use-theme)] + (into [quo.context/provider {:theme theme :screen-id stack-id}] + children))) + +(def views + {"communities.status" [stack + :screen/communities-stack + [communities/view]] + "messages.status" [stack + :screen/chats-stack + [chat/view]] + "wallet.status" [stack :screen/wallet-stack + [wallet/view]]}) + + +(def metadata + {"communities.status" {:title "Communities" + :origin-url "communities.status" + :icon :i/communities} + "messages.status" {:title "Messages" + :origin-url "messages.status" + :icon :i/messages} + "wallet.status" {:title "Wallet" + :origin-url "wallet.status" + :icon :i/wallet}}) diff --git a/src/status_im/contexts/browser/rpc_errors.cljs b/src/status_im/contexts/browser/rpc_errors.cljs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/status_im/contexts/browser/rpc_params.cljs b/src/status_im/contexts/browser/rpc_params.cljs new file mode 100644 index 00000000000..1ff71ef3fb9 --- /dev/null +++ b/src/status_im/contexts/browser/rpc_params.cljs @@ -0,0 +1,6 @@ +(ns status-im.contexts.browser.rpc-params + (:require [utils.hex :as hex])) + +(defn switch-ethereum-chain + [event] + (-> event :params first :chain-id hex/normalize-hex hex/hex-to-number)) diff --git a/src/status_im/contexts/browser/screenshots/context.cljs b/src/status_im/contexts/browser/screenshots/context.cljs new file mode 100644 index 00000000000..4e19a269084 --- /dev/null +++ b/src/status_im/contexts/browser/screenshots/context.cljs @@ -0,0 +1,47 @@ +(ns status-im.contexts.browser.screenshots.context + (:require + ["react" :as react] + [oops.core :as oops] + [react-native.core :as rn] + [status-im.navigation.screens :as screens])) + +(defonce ^:private context (react/createContext nil)) + +(defn provider + [data & children] + (let [[screenshots set-screenshots] (rn/use-state {}) + add-screenshot-url (fn [tab-id url] + (set-screenshots (fn [state] + (assoc-in state [tab-id :url] url)))) + add-ref (fn [tab-id ref] + (set-screenshots (fn [state] + (assoc-in state [tab-id :ref] ref))))] + (into [:> (.-Provider context) {:value #js {:cljData data}}] + children))) + +(def default-data + {:refs {}}) + +(defn- use-context-data + [] + (if-let [data (rn/use-context context)] + (oops/oget data :cljData) + default-data)) + +(defn use-ref + [] + (if-let [data (rn/use-context context)] + (:theme (oops/oget data :cljData)) + :light)) + +(defn use-screen-id + "A hook that returns the current screen id." + [] + (when-let [data (rn/use-context context)] + (:screen-id (oops/oget data :cljData)))) + +(defn use-screen-params + "A hook that returns the current screen params" + [] + (when-let [data (rn/use-context context)] + (:screen-params (oops/oget data :cljData)))) diff --git a/src/status_im/contexts/browser/screenshots/events.cljs b/src/status_im/contexts/browser/screenshots/events.cljs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/status_im/contexts/browser/subs/core.cljs b/src/status_im/contexts/browser/subs/core.cljs new file mode 100644 index 00000000000..ff7ab3c2c76 --- /dev/null +++ b/src/status_im/contexts/browser/subs/core.cljs @@ -0,0 +1,110 @@ +(ns status-im.contexts.browser.subs.core + (:require [re-frame.core :as rf])) + +(rf/reg-sub :browser/tab-ids + (fn [db] + (get db :browser/tab-ids))) + +(rf/reg-sub :browser/tabs-by-id + (fn [db] + (get db :browser/tabs-by-id))) + +(rf/reg-sub :browser/permissions + (fn [db] + (get db :browser/permissions))) + +(rf/reg-sub :browser/dapps + (fn [db] + (get db :browser/dapps))) + +(rf/reg-sub :browser/mode + (fn [db] + (get db :browser/mode))) + +(rf/reg-sub :browser/focused-tab-id + (fn [db] + (let [default (-> db :browser/tab-ids first)] + (get db :browser/focused-tab-id default)))) + +(rf/reg-sub :browser/focused-tab + :<- [:browser/tabs-by-id] + :<- [:browser/focused-tab-id] + (fn [[tabs-by-id focused-tab-id]] + (get tabs-by-id focused-tab-id))) + +(rf/reg-sub :browser/focused-tab-idx + :<- [:browser/focused-tab-id] + :<- [:browser/tab-ids] + (fn [[focused-id tab-ids]] + (let [idx (.indexOf tab-ids focused-id)] + (when (not= -1 idx) idx)))) + +(rf/reg-sub :browser/can-focus-next? + :<- [:browser/focused-tab-idx] + :<- [:browser/tab-ids] + (fn [[focused-idx tab-ids]] + (not= (inc focused-idx) (count tab-ids)))) + +(rf/reg-sub :browser/can-focus-prev? + :<- [:browser/focused-tab-idx] + (fn [focused-idx] + (not (zero? focused-idx)))) + +(rf/reg-sub :browser/tab-by-id + :<- [:browser/tabs-by-id] + (fn [tabs-by-id [_ tab-id]] + (get tabs-by-id tab-id))) + +(defn- tab-by-id-query + [[_ tab-id]] + [(rf/subscribe [:browser/tab-by-id tab-id])]) + +(rf/reg-sub :browser/tabs + :<- [:browser/tab-ids] + :<- [:browser/tabs-by-id] + (fn [[tab-ids tabs-by-id]] + (->> tab-ids + (map (partial get tabs-by-id))))) + +(rf/reg-sub :browser/tab-url + tab-by-id-query + (fn [[tab-data]] + (:url tab-data))) + +(rf/reg-sub :browser/tab-type + tab-by-id-query + (fn [[tab-data]] + (:type tab-data))) + +(rf/reg-sub :browser/tab-type + tab-by-id-query + (fn [[tab-data]] + (:type tab-data))) + +(rf/reg-sub :browser/dapp-for-tab + :<- [:browser/tabs-by-id] + :<- [:browser/dapps] + (fn [[tabs dapps] [_ tab-id]] + (let [dapp-id (get-in tabs [tab-id :dapp-id]) + dapp (get dapps dapp-id)] + dapp))) + +(defn- dapp-by-tab-id-query + [[_ tab-id]] + [(rf/subscribe [:browser/dapp-for-tab tab-id])]) + +(rf/reg-sub :browser/tab-title + dapp-by-tab-id-query + (fn [[dapp]] + (let [title (-> dapp :metadata :title) + url (-> dapp :origin-url)] + (or title url)))) + +(rf/reg-sub :browser/screenshots + (fn [db] + (get db :browser/screenshots))) + +(rf/reg-sub :browser/tab-screenshot + :<- [:browser/screenshots] + (fn [screenshots [_ tab-id]] + (get-in screenshots [tab-id :url]))) diff --git a/src/status_im/contexts/browser/tabs/tab_window.cljs b/src/status_im/contexts/browser/tabs/tab_window.cljs new file mode 100644 index 00000000000..35368cc87ff --- /dev/null +++ b/src/status_im/contexts/browser/tabs/tab_window.cljs @@ -0,0 +1,117 @@ +(ns status-im.contexts.browser.tabs.tab-window + (:require [quo.context :as quo.context] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.freeze :as freeze] + [react-native.reanimated :as reanimated] + [react-native.safe-area :as safe-area] + [react-native.view-shot :as view-shot] + [reagent.core :as reagent] + [status-im.contexts.browser.constants :as browser.constants] + [status-im.contexts.browser.core :as browser] + [status-im.contexts.browser.hooks :as hooks] + [status-im.contexts.browser.native-dapps :as native-dapps] + [status-im.contexts.browser.tabs.webview :as webview] + [utils.re-frame :as rf])) + +(defn interpolate-x-position + [x-value tab-idx output-values] + (let [x-tab (browser/tab-position tab-idx) + x-tab-prev (browser/tab-position (dec tab-idx)) + x-tab-next (browser/tab-position (inc tab-idx))] + (reanimated/interpolate x-value + [x-tab-prev x-tab x-tab-next] + output-values + {:extrapolateLeft "clamp" + :extrapolateRight "clamp"}))) + +(defn tab-container + [{:keys [idx x-translation-value]} & children] + (let [theme (quo.context/use-theme) + scale-down-amount 0.95] + (into [reanimated/view + {:style + [{:transform [{:scale (interpolate-x-position x-translation-value + idx + [scale-down-amount 1 scale-down-amount])}]} + {:width browser.constants/browser-width + :height browser.constants/browser-height + :border-radius 20 + :transform-origin :bottom + :overflow :hidden + :background-color (colors/theme-colors colors/white colors/neutral-95 theme)}]}] + children))) + +(defn freeze-placeholder + [tab-id] + (let [screenshot-url (rf/sub [:browser/tab-screenshot tab-id])] + [rn/view + {:style + {:width browser.constants/browser-width + :height browser.constants/browser-height + :border-radius 20}} + [rn/image + {:source screenshot-url + :style {:flex 1}}]])) + +(defn view + [tab-id idx x-translation-value] + (let [url (rf/sub [:browser/tab-url tab-id]) + tab-type (rf/sub [:browser/tab-type tab-id]) + focused-tab-idx (rf/sub [:browser/focused-tab-idx]) + ;;freeze-tab? (browser/freeze-tab? idx focused-tab-idx) + unfocused? (not= idx focused-tab-idx) + {:keys [ref options capture]} (hooks/use-screenshot-tab tab-id) + screenshot-interval (rn/use-ref-atom nil) + [freeze? set-freeze] (rn/use-state unfocused?) + placeholder-opacity-value (reanimated/use-shared-value (if unfocused? 1 0))] + + (rn/use-effect (fn [] + (reanimated/animate-shared-value-with-delay placeholder-opacity-value + (if unfocused? 1 0) + 200 + :easing2 + 200) + (js/setTimeout (fn [] (set-freeze unfocused?)) 400)) + [unfocused?]) + + (rn/use-effect (fn [] + (when-not freeze? + (capture))) + [freeze?]) + + ;; (rn/use-unmount (fn [] (js/clearInterval @screenshot-interval))) + ;; (rn/use-effect (fn [] + ;; (if freeze? + ;; (when @screenshot-interval + ;; (js/clearInterval @screenshot-interval)) + ;; (let [interval-id (js/setInterval capture 5000)] + ;; (capture) + ;; (reset! screenshot-interval interval-id)))) + ;; [freeze?]) + [view-shot/view + {:ref ref + :style {:flex 1} + :options options} + [tab-container + {:idx idx + :x-translation-value x-translation-value} + [freeze/view + {:freeze unfocused? + :placeholder (reagent/as-element + [freeze-placeholder tab-id])} + (if (= :tab/native tab-type) + [rn/view + {:style {:margin-top (- 10 safe-area/top) + :z-index 5 + :flex 1}} + (get native-dapps/views url)] + [webview/view + {:tab-id tab-id + :url url}])] + (when freeze? + [reanimated/view + {:pointer-events :none + :style [{:opacity placeholder-opacity-value} + {:position :absolute :left 0 :right 0 :bottom 0 :top 0}]} + [freeze-placeholder tab-id]])]])) diff --git a/src/status_im/contexts/browser/tabs/tabs_preview.cljs b/src/status_im/contexts/browser/tabs/tabs_preview.cljs new file mode 100644 index 00000000000..5ba559e7dab --- /dev/null +++ b/src/status_im/contexts/browser/tabs/tabs_preview.cljs @@ -0,0 +1,92 @@ +(ns status-im.contexts.browser.tabs.tabs-preview + (:require [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.reanimated :as reanimated] + [react-native.safe-area :as safe-area] + [status-im.contexts.browser.components.dapp-icon :as dapp-icon] + [status-im.contexts.browser.constants :as browser.constants] + [utils.re-frame :as rf])) + +(def preview-width + (/ (- browser.constants/browser-width (* 3 20)) 2)) + +(def preview-height (* preview-width 1.2)) + +(defn on-press-preview + [tab-id] + (rf/dispatch [:browser/show-browser]) + (rf/dispatch [:browser/focus-tab tab-id])) + +(defn tab-preview + [tab-id] + (let [dapp (rf/sub [:browser/dapp-for-tab tab-id]) + preview-url (rf/sub [:browser/tab-screenshot tab-id])] + [rn/pressable + {:on-press #(on-press-preview tab-id) + :style {:align-items :center + :justify-content :center + :background-color colors/neutral-80 + :border-radius 12 + :width preview-width + :height preview-height}} + [rn/image + {:source {:uri preview-url} + :resize-mode :cover + :style {:border-radius 12 + :width preview-width + :height preview-height}}] + [rn/view + {:style {:position :absolute + :bottom -8 + :left 0 + :right 0 + :justify-content :center + :align-items :center}} + [rn/view + {:style {:width 32 + :height 32 + :background-color colors/neutral-20 + :border-radius 20 + :justify-content :center + :align-items :center}} + [dapp-icon/view + {:dapp dapp + :size 24 + :background-color colors/neutral-80}]]]])) + +(defn use-hide + [show?] + (let [opacity-value (reanimated/use-shared-value (if show? 1 0))] + (rn/use-effect + (fn [] + (if show? + (reanimated/animate-shared-value-with-timing opacity-value 1 200 :easing2) + (reanimated/animate-shared-value-with-timing opacity-value 0 200 :easing2))) + [show?]) + {:opacity opacity-value})) + +(defn view + [] + (let [tab-ids (rf/sub [:browser/tab-ids]) + browser-mode (rf/sub [:browser/mode]) + tabs-style (use-hide (= browser-mode :browser-mode/tabs))] + (when (= :browser-mode/tabs browser-mode) + [reanimated/view + {:style [tabs-style + {:position :absolute + :top safe-area/top + :left 0 + :right 0 + :bottom (+ browser.constants/footer-height safe-area/bottom)}]} + [rn/scroll-view + {:shows-horizontal-scroll-indicator false + :shows-vertical-scroll-indicator false + :content-container-style {:padding-horizontal 20 + :padding-vertical 20 + :flex-direction :row + :flex-wrap :wrap + :gap 20}} + (map-indexed (fn [idx tab-id] + ^{:key (str idx "-" tab-id)} + [tab-preview tab-id]) + tab-ids)]]))) diff --git a/src/status_im/contexts/browser/tabs/view.cljs b/src/status_im/contexts/browser/tabs/view.cljs new file mode 100644 index 00000000000..12dae23be1f --- /dev/null +++ b/src/status_im/contexts/browser/tabs/view.cljs @@ -0,0 +1,39 @@ +(ns status-im.contexts.browser.tabs.view + (:require [react-native.core :as rn] + [react-native.reanimated :as reanimated] + [status-im.contexts.browser.tabs.tab-window :as tab-window] + [utils.re-frame :as rf])) + +(defn use-transition-animation + [show?] + (let [opacity-value (reanimated/use-shared-value (if show? 1 0)) + scale-value (reanimated/use-shared-value (if show? 1 0.8))] + (rn/use-effect + (fn [] + (if show? + (do (reanimated/animate-shared-value-with-timing opacity-value 1 200 :easing2) + (reanimated/animate-shared-value-with-timing scale-value 1 200 :easing2)) + (do (reanimated/animate-shared-value-with-timing opacity-value 0 600 :easing2) + (reanimated/animate-shared-value-with-timing scale-value 0.6 200 :easing2)))) + [show?]) + {:opacity (reanimated/interpolate scale-value [0.6 0.8 1] [0 0.7 1]) + :transform-origin :top + :transform [{:scale scale-value}]})) + +(defn view + [{:keys [scroll-ref x-translation-value]}] + (let [browser-mode (rf/sub [:browser/mode]) + tab-ids (rf/sub [:browser/tab-ids]) + browser-style (use-transition-animation (= browser-mode :browser-mode/browser))] + [reanimated/view + {:style [browser-style {:flex 1}]} + [reanimated/scroll-view + {:horizontal true + :ref scroll-ref + :shows-horizontal-scroll-indicator false + :shows-vertical-scroll-indicator false + :scroll-enabled false} + (map-indexed (fn [idx tab-id] + ^{:key (str idx "-" tab-id)} + [tab-window/view tab-id idx x-translation-value]) + tab-ids)]])) diff --git a/src/status_im/contexts/browser/tabs/webview.cljs b/src/status_im/contexts/browser/tabs/webview.cljs new file mode 100644 index 00000000000..ae4e65ad4fa --- /dev/null +++ b/src/status_im/contexts/browser/tabs/webview.cljs @@ -0,0 +1,55 @@ +(ns status-im.contexts.browser.tabs.webview + (:require [react-native.webview :as webview] + [status-im.contexts.browser.js-scripts.core :as js-scripts] + [utils.re-frame :as rf])) + +(defn make-injected-script + [tab-id] + (-> + "" + (js-scripts/add-global-var "WALLET_UUID" tab-id) + (js-scripts/add-global-var "WALLET_NAME" "Status") + (js-scripts/add-global-var + "WALLET_ICON" + "https://play-lh.googleusercontent.com/VZTM4ybMq2LtICfiF_nYOlId_TfgVo1rgACYrPSUqcuY4MplG-CvWw-1dN4XPKTNixmc=w240-h480-rw") + (js-scripts/add-global-var "WALLET_RDNS" "im.status.ethereum") + (js-scripts/add-script js-scripts/website-metadata) + (js-scripts/add-script js-scripts/web3-provider))) + +(defn view + [{:keys [tab-id url]}] + [webview/view + {:ref #(rf/dispatch [:browser/set-tab-ref tab-id %]) + :container-style {:border-radius 20} + :source {:uri url} + :java-script-enabled true + :bounces false + :cache-enabled true + :local-storage-enabled true + :set-support-multiple-windows false + ;;:injected-java-script js-scripts/website-metadata + :injected-java-script-before-content-loaded (make-injected-script tab-id) + :on-message #(rf/dispatch [:browser/on-message tab-id %]) + :on-error #(println :on-error %) + :allows-back-forward-navigation-gestures true ;; iOS only + :pull-to-refresh-enabled true + :webview-debugging-enabled true + ;; https://github.com/status-im/status-mobile/issues/17854 + :allows-inline-media-playback true + :mixed-content-mode :always + :origin-white-list ["*"] + :user-agent + "Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.103 Mobile Safari/537.36" + ;;:render-error web-view-error + ;;:on-navigation-state-change #() + ;; :on-message #(re-frame/dispatch + ;; [:browser/bridge-message-received + ;; (.. ^js % -nativeEvent + ;; -data)]) + ;; :on-load #(re-frame/dispatch [:browser/loading-started]) + ;; :on-error #(re-frame/dispatch [:browser/error-occured]) + ;; :injected-java-script-before-content-loaded (js-res/ethereum-provider (str network-id)) + ;; https://github.com/status-im/status-mobile/issues/17854 + }]) +( +) diff --git a/src/status_im/contexts/browser/view.cljs b/src/status_im/contexts/browser/view.cljs new file mode 100644 index 00000000000..316c7bd875e --- /dev/null +++ b/src/status_im/contexts/browser/view.cljs @@ -0,0 +1,30 @@ +(ns status-im.contexts.browser.view + (:require [quo.foundations.colors :as colors] + [react-native.core :as rn] + [react-native.gesture :as gesture] + [react-native.reanimated :as reanimated] + [react-native.safe-area :as safe-area] + [status-im.contexts.browser.footer.view :as footer] + [status-im.contexts.browser.hooks :as hooks] + [status-im.contexts.browser.tabs.tabs-preview :as tabs-preview] + [status-im.contexts.browser.tabs.view :as tabs])) + +(defn view + [] + (let [scroll-ref (reanimated/use-animated-ref) + {:keys [gesture-prop x-translation-value]} (hooks/use-swipe-gesture scroll-ref)] + [rn/view + {:style {:background-color colors/neutral-100 + :padding-top safe-area/top + :flex 1}} + ;; NOTE: the browser tab windows live in a scroll-view (with disabled gestures) inside + ;; `tabs/view`. The scrolling is handled by the `gesture-detector`, which wraps the + ;; `footer/view`. + [tabs/view + {:scroll-ref scroll-ref + :x-translation-value x-translation-value}] + ;; NOTE: tabs preview is absolutely positioned inside `tabs-preview/view` and is conditionally + ;; rendered depending on the `:browser/mode` state + [tabs-preview/view] + [gesture/gesture-detector {:gesture gesture-prop} + [footer/view]]])) diff --git a/src/status_im/contexts/keycard/pin/view.cljs b/src/status_im/contexts/keycard/pin/view.cljs index 517e32955b7..ea8521e0077 100644 --- a/src/status_im/contexts/keycard/pin/view.cljs +++ b/src/status_im/contexts/keycard/pin/view.cljs @@ -13,6 +13,7 @@ pin-retry-counter (rf/sub [:keycard/pin-retry-counter]) error? (or error? (= status :error))] (rn/use-unmount #(rf/dispatch [:keycard.pin/clear])) + (rn/use-mount #(on-complete "178717")) [rn/view {:style {:flex 1 :gap 34 diff --git a/src/status_im/contexts/profile/config.cljs b/src/status_im/contexts/profile/config.cljs index 36dedee9424..2b635aec508 100644 --- a/src/status_im/contexts/profile/config.cljs +++ b/src/status_im/contexts/profile/config.cljs @@ -32,7 +32,8 @@ :alchemyArbitrumMainnetToken config/ALCHEMY_ARBITRUM_MAINNET_TOKEN :alchemyArbitrumSepoliaToken config/ALCHEMY_ARBITRUM_SEPOLIA_TOKEN :alchemyBaseMainnetToken config/ALCHEMY_BASE_MAINNET_TOKEN - :alchemyBaseSepoliaToken config/ALCHEMY_BASE_SEPOLIA_TOKEN}) + :alchemyBaseSepoliaToken config/ALCHEMY_BASE_SEPOLIA_TOKEN + :apiConfig {:connectorEnabled true}}) (defn- common-config [] diff --git a/src/status_im/contexts/profile/login/events.cljs b/src/status_im/contexts/profile/login/events.cljs index 6802e49fca7..606baa247c6 100644 --- a/src/status_im/contexts/profile/login/events.cljs +++ b/src/status_im/contexts/profile/login/events.cljs @@ -131,6 +131,7 @@ (fn [{:keys [db]}] (let [{:keys [preview-privacy?]} (:profile/profile db)] {:fx [[:browser/initialize-browser] + [:dispatch [:browser/init]] [:logging/initialize-web3-client-version] [:group-chats/get-group-chat-invitations] [:profile.settings/blank-preview-flag-changed preview-privacy?] diff --git a/src/status_im/contexts/shell/bottom_tabs/view.cljs b/src/status_im/contexts/shell/bottom_tabs/view.cljs index e052175919c..5c8262ac363 100644 --- a/src/status_im/contexts/shell/bottom_tabs/view.cljs +++ b/src/status_im/contexts/shell/bottom_tabs/view.cljs @@ -5,7 +5,6 @@ [react-native.core :as rn] [react-native.gesture :as gesture] [react-native.reanimated :as reanimated] - [status-im.config :as config] [status-im.contexts.shell.bottom-tabs.style :as style] [status-im.contexts.shell.constants :as shell.constants] [status-im.feature-flags :as ff] @@ -52,5 +51,4 @@ [bottom-tab :i/messages :screen/chats-stack shared-values]] [gesture/gesture-detector {:gesture communities-double-tab-gesture} [bottom-tab :i/communities :screen/communities-stack shared-values]] - (when config/show-not-implemented-features? - [bottom-tab :i/browser :screen/browser-stack shared-values])]]])) + [bottom-tab :i/browser :screen/browser-stack shared-values]]]])) diff --git a/src/status_im/contexts/shell/home_stack/view.cljs b/src/status_im/contexts/shell/home_stack/view.cljs index 902b679a22a..66baca85a6b 100644 --- a/src/status_im/contexts/shell/home_stack/view.cljs +++ b/src/status_im/contexts/shell/home_stack/view.cljs @@ -1,9 +1,9 @@ (ns status-im.contexts.shell.home-stack.view (:require - [legacy.status-im.ui.screens.browser.stack :as browser.stack] [quo.context] [react-native.core :as rn] [react-native.reanimated :as reanimated] + [status-im.contexts.browser.view :as new-browser] [status-im.contexts.chat.home.view :as chat] [status-im.contexts.communities.home.view :as communities] [status-im.contexts.market.view :as market] @@ -35,21 +35,21 @@ :screen/chats-stack [chat/view] :screen/wallet-stack [wallet/view] :screen/market-stack [market/view] - :screen/browser-stack [browser.stack/browser-stack] + :screen/browser-stack [new-browser/view] [:<>])]) (defn lazy-screen [stack-id shared-values theme] (when (load-stack? stack-id) - [quo.context/provider {:theme theme :screen-id stack-id} + [quo.context/provider {:theme theme} [f-stack-view stack-id shared-values]])) (defn view [shared-values] (let [theme (quo.context/use-theme)] [rn/view {:style (style/home-stack theme)} - [lazy-screen :screen/communities-stack shared-values theme] - [lazy-screen :screen/chats-stack shared-values theme] + #_[lazy-screen :screen/communities-stack shared-values theme] + #_[lazy-screen :screen/chats-stack shared-values theme] [lazy-screen :screen/browser-stack shared-values theme] - [lazy-screen :screen/wallet-stack shared-values theme] - [lazy-screen :screen/market-stack shared-values theme]])) + #_[lazy-screen :screen/wallet-stack shared-values theme] + #_[lazy-screen :screen/market-stack shared-values theme]])) diff --git a/src/status_im/contexts/shell/view.cljs b/src/status_im/contexts/shell/view.cljs index 63d1a8a437e..432021a5368 100644 --- a/src/status_im/contexts/shell/view.cljs +++ b/src/status_im/contexts/shell/view.cljs @@ -13,8 +13,8 @@ [] (when (or (seq @navigation.state/modals) (> (count (navigation.state/get-navigation-state)) 1)) - (rf/dispatch [:navigate-back]) - true)) + (rf/dispatch [:navigate-back])) + true) ;; A hidden view-id-tracker view, required for e2e testing (defn- view-id-tracker @@ -37,4 +37,4 @@ [home-stack/view shared-values] (when config/enable-view-id-tracker? [view-id-tracker]) - [bottom-tabs/view shared-values]])) + #_[bottom-tabs/view shared-values]])) diff --git a/src/status_im/contexts/wallet/collectible/tabs/overview/view.cljs b/src/status_im/contexts/wallet/collectible/tabs/overview/view.cljs index 8dc2a42ca8a..12f9bb428b7 100644 --- a/src/status_im/contexts/wallet/collectible/tabs/overview/view.cljs +++ b/src/status_im/contexts/wallet/collectible/tabs/overview/view.cljs @@ -61,7 +61,7 @@ [collectible] (let [owner-address (or (rf/sub [:wallet/current-viewing-account-address]) (-> collectible :ownership first :address)) - owner-account (rf/sub [:wallet-connect/account-details-by-address owner-address]) + owner-account (rf/sub [:dapps/account-details-by-address owner-address]) traits (-> collectible :collectible-data :traits)] [:<> [info diff --git a/src/status_im/contexts/wallet/wallet_connect/modals/session_proposal/view.cljs b/src/status_im/contexts/wallet/wallet_connect/modals/session_proposal/view.cljs index 245066be89c..d419ebce586 100644 --- a/src/status_im/contexts/wallet/wallet_connect/modals/session_proposal/view.cljs +++ b/src/status_im/contexts/wallet/wallet_connect/modals/session_proposal/view.cljs @@ -88,7 +88,7 @@ all-networks-in-session?]} (rf/sub [:wallet-connect/session-proposal-network-details]) address (rf/sub [:wallet-connect/current-proposal-address]) - {:keys [name customization-color emoji]} (rf/sub [:wallet-connect/account-details-by-address + {:keys [name customization-color emoji]} (rf/sub [:dapps/account-details-by-address address]) network-names (->> session-networks (map format-network-name) diff --git a/src/status_im/contexts/wallet/wallet_connect/modals/session_proposal/view.cljs~ b/src/status_im/contexts/wallet/wallet_connect/modals/session_proposal/view.cljs~ new file mode 100644 index 00000000000..245066be89c --- /dev/null +++ b/src/status_im/contexts/wallet/wallet_connect/modals/session_proposal/view.cljs~ @@ -0,0 +1,163 @@ +(ns status-im.contexts.wallet.wallet-connect.modals.session-proposal.view + (:require + [clojure.string :as string] + [quo.context] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.common.floating-button-page.view :as floating-button-page] + [status-im.contexts.wallet.wallet-connect.modals.common.list-info-box.view :as list-info-box] + [status-im.contexts.wallet.wallet-connect.modals.session-proposal.style :as style] + [status-im.contexts.wallet.wallet-connect.utils.data-store :as data-store] + [utils.i18n :as i18n] + [utils.re-frame :as rf] + [utils.string])) + +(defn- dapp-metadata + [] + (let [proposer (rf/sub [:wallet-connect/session-proposer]) + {:keys [icons name url]} (:metadata proposer) + first-icon (first icons) + dapp-name (data-store/compute-dapp-name name url) + profile-picture (data-store/compute-dapp-icon-path first-icon url)] + [:<> + [rn/view {:style style/dapp-avatar} + [quo/user-avatar + {:profile-picture profile-picture + :size :big + :full-name dapp-name}]] + [quo/page-top + {:title dapp-name + :description :context-tag + :context-tag {:type :icon + :size 32 + :icon :i/link + :context url}}]])) + +(defn- approval-note + [] + (let [dapp-name (rf/sub [:wallet-connect/session-proposer-name])] + [list-info-box/view + {:dapp-name dapp-name + :container-style {:margin-horizontal 20}}])) + +(defn- format-network-name + [network] + (-> network :network-name name string/capitalize)) + +(defn- set-current-proposal-address + [acc] + (fn [] + (rf/dispatch [:wallet-connect/set-current-proposal-address (:address acc)]) + (rf/dispatch [:hide-bottom-sheet]))) + +(defn- accounts-list + [] + (let [accounts (rf/sub [:wallet/operable-accounts]) + selected-address (rf/sub [:wallet-connect/current-proposal-address])] + [rn/view {:style style/account-switcher-list} + (for [{:keys [address] :as account} accounts] + ^{:key (str address)} + [quo/account-item + {:type :default + :state (if (= address selected-address) + :selected + :default) + :account-props account + :on-press (set-current-proposal-address account)}])])) + +(defn- account-switcher-sheet + [] + [:<> + [rn/view {:style style/account-switcher-title} + [quo/text + {:size :heading-2 + :weight :semi-bold + :accessibility-label "select-account-title"} + (i18n/label :t/select-account)]] + [accounts-list]]) + +(defn- show-account-switcher-bottom-sheet + [] + (rf/dispatch + [:show-bottom-sheet + {:content account-switcher-sheet}])) + +(defn- connection-category + [] + (let [{:keys [session-networks + all-networks-in-session?]} (rf/sub + [:wallet-connect/session-proposal-network-details]) + address (rf/sub [:wallet-connect/current-proposal-address]) + {:keys [name customization-color emoji]} (rf/sub [:wallet-connect/account-details-by-address + address]) + network-names (->> session-networks + (map format-network-name) + (string/join ", ")) + network-images (mapv :source session-networks) + data-item-common-props {:blur? false + :card? false + :status :default + :size :large} + account-data-item-props (assoc data-item-common-props + :right-content {:type :accounts + :size :size-32 + :data [{:emoji emoji + :customization-color + customization-color}]} + :on-press show-account-switcher-bottom-sheet + :title (i18n/label :t/account-title) + :subtitle name + :right-icon :i/chevron-right) + networks-data-item-props (assoc data-item-common-props + :right-content {:type :network + :data network-images} + :title (i18n/label :t/networks) + :subtitle (if all-networks-in-session? + (i18n/label :t/all-networks) + network-names))] + [quo/category + {:blur? false + :list-type :data-item + :data [account-data-item-props + networks-data-item-props]}])) + +(defn- footer + [] + (let [customization-color (rf/sub [:profile/customization-color])] + [quo/bottom-actions + {:actions :two-actions + :buttons-container-style style/footer-buttons-container + :button-two-label (i18n/label :t/decline) + :button-two-props {:type :grey + :accessibility-label :wc-deny-connection + :on-press #(rf/dispatch + [:dismiss-modal + :screen/wallet.wallet-connect-session-proposal])} + :button-one-label (i18n/label :t/connect) + :button-one-props {:customization-color customization-color + :type :primary + :accessibility-label :wc-connect + :on-press #(rf/dispatch + [:wallet-connect/approve-session])}}])) + +(defn- header + [] + [quo/page-nav + {:type :no-title + :background :blur + :icon-name :i/close + :on-press (rn/use-callback + #(rf/dispatch [:dismiss-modal :screen/wallet.wallet-connect-session-proposal])) + :accessibility-label :wc-session-proposal-top-bar}]) + +(defn view + [] + (rn/use-unmount #(rf/dispatch [:wallet-connect/reject-session-proposal])) + [floating-button-page/view + {:footer-container-padding 0 + :header [header] + :footer [footer]} + [rn/view + [dapp-metadata] + [connection-category] + [approval-note]]]) diff --git a/src/status_im/db.cljs b/src/status_im/db.cljs index f240e145809..ece01cad9c2 100644 --- a/src/status_im/db.cljs +++ b/src/status_im/db.cljs @@ -10,6 +10,8 @@ (def app-db {:activity-center {:filter {:status (:filter-status activity-center/defaults) :type (:filter-type activity-center/defaults)}} + :browser/tab-ids [] + :browser/tabs-by-id {} :contacts/contacts {} :pairing/installations {} :group/selected-contacts #{} diff --git a/src/status_im/effects.cljs b/src/status_im/effects.cljs new file mode 100644 index 00000000000..00c8d6e8fa0 --- /dev/null +++ b/src/status_im/effects.cljs @@ -0,0 +1,12 @@ +(ns status-im.effects + (:require [promesa.core :as promesa] + [utils.re-frame :as rf])) + +(rf/reg-fx :fx.promise + (fn [{:keys [promise args on-success on-error] + :or {args [] + on-success identity + on-error identity}}] + (-> (apply promise args) + (promesa/then (partial rf/call-continuation on-success)) + (promesa/catch (partial rf/call-continuation on-error))))) diff --git a/src/status_im/events.cljs b/src/status_im/events.cljs index 204112f5129..a5141421f7b 100644 --- a/src/status_im/events.cljs +++ b/src/status_im/events.cljs @@ -17,6 +17,7 @@ status-im.common.theme.events [status-im.common.toasts.events] status-im.common.universal-links + status-im.contexts.browser.events.core status-im.contexts.chat.contacts.events status-im.contexts.chat.events [status-im.contexts.chat.home.add-new-contact.events] @@ -57,6 +58,7 @@ status-im.contexts.wallet.swap.events status-im.contexts.wallet.wallet-connect.events.core [status-im.db :as db] + status-im.effects status-im.navigation.effects status-im.navigation.events [utils.re-frame :as rf])) diff --git a/src/status_im/navigation/events.cljs b/src/status_im/navigation/events.cljs index 721338b842c..0704349acdd 100644 --- a/src/status_im/navigation/events.cljs +++ b/src/status_im/navigation/events.cljs @@ -115,16 +115,29 @@ {:events [:show-bottom-sheet]} [{:keys [db] :as cofx} content] (let [theme (or (:theme content) (:theme db)) + ;; NOTE: if true, allows to return the bottom-sheet that was being shown after the stacked + ;; one is hidden + {:keys [stacked?]} content {:keys [sheets hide?]} (:bottom-sheet db)] (rf/merge cofx - {:db (update-in db [:bottom-sheet :sheets] conj content) + {:db (if stacked? + (update-in db [:bottom-sheet :sheets] concat [content]) + (update-in db [:bottom-sheet :sheets] conj content)) :dismiss-keyboard nil} (fn [new-cofx] (when-not hide? - (if (seq sheets) + (if (and (not stacked?) (seq sheets)) (hide-bottom-sheet new-cofx) {:show-bottom-sheet {:theme theme}})))))) +;; (rf/defn show-nested-bottom-sheet +;; {:events [:show-nested-bottom-sheet]} +;; [{:keys [db] :as cofx} content] +;; (let [theme (or (:theme content) (:theme db)) +;; {:keys [sheets hide?]} (:bottom-sheet db)] +;; (rf/merge cofx +;; {:db (update-in db [:bottom-sheet :sheets] conj content)}))) + (rf/defn set-view-id {:events [:set-view-id]} [{:keys [db]} view-id] diff --git a/src/status_im/subs/root.cljs b/src/status_im/subs/root.cljs index e2645cd848d..0ec085c9d5e 100644 --- a/src/status_im/subs/root.cljs +++ b/src/status_im/subs/root.cljs @@ -1,6 +1,7 @@ (ns status-im.subs.root (:require [re-frame.core :as re-frame] + status-im.contexts.browser.subs.core status-im.subs.activity-center status-im.subs.alert-banner status-im.subs.biometrics diff --git a/src/status_im/subs/wallet/dapps/core.cljs b/src/status_im/subs/wallet/dapps/core.cljs index 252a3393bdf..1114d1282a5 100644 --- a/src/status_im/subs/wallet/dapps/core.cljs +++ b/src/status_im/subs/wallet/dapps/core.cljs @@ -8,7 +8,7 @@ [utils.string])) (rf/reg-sub - :wallet-connect/account-details-by-address + :dapps/account-details-by-address :<- [:wallet/accounts-without-watched-accounts] (fn [accounts [_ address]] (let [{:keys [customization-color name emoji]} (wallet-utils/get-account-by-address accounts address)] diff --git a/src/utils/hex.cljs b/src/utils/hex.cljs index 649ca121e9f..3e34d24b1f0 100644 --- a/src/utils/hex.cljs +++ b/src/utils/hex.cljs @@ -37,3 +37,14 @@ [:=> [:cat [:or :string :int]] :string]) + +(defn hex-to-number + [value] + (->> value + normalize-hex + native-module/hex-to-number)) + +(schema/=> hex-to-number + [:=> + [:cat [:or :string :int]] + :int]) diff --git a/src/utils/slurp.clj b/src/utils/slurp.clj new file mode 100644 index 00000000000..66a06b192e7 --- /dev/null +++ b/src/utils/slurp.clj @@ -0,0 +1,6 @@ +(ns utils.slurp + (:refer-clojure :exclude [slurp])) + +(defmacro slurp + [file] + (clojure.core/slurp file)) diff --git a/src/utils/worklets/browser.cljs b/src/utils/worklets/browser.cljs new file mode 100644 index 00000000000..2444d36d60d --- /dev/null +++ b/src/utils/worklets/browser.cljs @@ -0,0 +1,21 @@ +(ns utils.worklets.browser + (:require [goog.object :as gobj] + [react-native.utils :as utils])) + +(def ^:private worklets (js/require "../src/js/worklets/browser.js")) + +(defn- transform-args + [f] + (fn [& args] + (apply f (map utils/kebab-case-map->camelCase-obj args)))) + +(defn- worklet-wrapper + [worklet-name] + (if-let [worklet-fn (gobj/get worklets worklet-name)] + (transform-args worklet-fn) + (throw (js/Error. + (ex-info "Non-existing worklet!" + {:name worklet-name + :file "../src/js/worklets/browser.js"}))))) + +(def use-scroll-tab (worklet-wrapper "useScrollTab")) diff --git a/yarn.lock b/yarn.lock index 714506024fa..4584ea607bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4870,6 +4870,11 @@ base-64@^1.0.0: resolved "https://registry.yarnpkg.com/base-64/-/base-64-1.0.0.tgz#09d0f2084e32a3fd08c2475b973788eee6ae8f4a" integrity sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg== +base64-arraybuffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc" + integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ== + base64-js@^1.0.2: version "1.3.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" @@ -5726,6 +5731,13 @@ crypto-random-string@^2.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== +css-line-break@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0" + integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w== + dependencies: + utrie "^1.0.2" + css-select@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" @@ -6971,6 +6983,14 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +html2canvas@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543" + integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA== + dependencies: + css-line-break "^2.1.0" + text-segmentation "^1.0.3" + http-cache-semantics@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" @@ -9807,6 +9827,11 @@ react-dom@18.0.0: loose-envify "^1.1.0" scheduler "^0.21.0" +react-freeze@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.4.tgz#cbbea2762b0368b05cbe407ddc9d518c57c6f3ad" + integrity sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA== + react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -10055,11 +10080,6 @@ react-native-svg@13.10.0: css-select "^5.1.0" css-tree "^1.1.3" -react-native-system-navigation-bar@^2.6.4: - version "2.6.4" - resolved "https://registry.yarnpkg.com/react-native-system-navigation-bar/-/react-native-system-navigation-bar-2.6.4.tgz#34edee7051dea01531ff2be95dc14f9fa8a540b7" - integrity sha512-4pysgADW53PiuHv+2glzNLJnHSxqDszZvLoitLFI5os4D+gCDfxmR36VSET4EnXkzSf8X9mbeFkHYDypDHJyZA== - react-native-url-polyfill@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/react-native-url-polyfill/-/react-native-url-polyfill-2.0.0.tgz#db714520a2985cff1d50ab2e66279b9f91ffd589" @@ -10067,6 +10087,13 @@ react-native-url-polyfill@2.0.0: dependencies: whatwg-url-without-unicode "8.0.0-3" +react-native-view-shot@^3.8.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/react-native-view-shot/-/react-native-view-shot-3.8.0.tgz#1aa1905f0e79428ca32bf80c16fd4abc719c600b" + integrity sha512-4cU8SOhMn3YQIrskh+5Q8VvVRxQOu8/s1M9NAL4z5BY1Rm0HXMWkQJ4N0XsZ42+Yca+y86ISF3LC5qdLPvPuiA== + dependencies: + html2canvas "^1.4.1" + react-native-webview@13.6.3: version "13.6.3" resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-13.6.3.tgz#f3d26e942ef5cc5a07547f2e47903aa81a68e25e" @@ -11122,6 +11149,13 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" +text-segmentation@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943" + integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw== + dependencies: + utrie "^1.0.2" + thread-stream@^0.15.1: version "0.15.2" resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-0.15.2.tgz#fb95ad87d2f1e28f07116eb23d85aba3bc0425f4" @@ -11499,6 +11533,13 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== +utrie@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645" + integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw== + dependencies: + base64-arraybuffer "^1.0.2" + uuid@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"