diff --git a/.env.prod b/.env.prod index d0204dd8ae..5800678c76 100644 --- a/.env.prod +++ b/.env.prod @@ -20,3 +20,4 @@ GROUP_CHATS_ENABLED=0 USE_SYM_KEY=0 MAINNET_WARNING_ENABLED=1 SPAM_BUTTON_DETECTION_ENABLED=1 +UNIVERSAL_LINKS_ENABLED=0 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f4ceb43595..7f60c3df36 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -54,6 +54,14 @@ + + + + + + + + > selected-contacts @@ -407,13 +410,16 @@ (navigate-to-chat random-id {}) (transport.message/send (group-chat/GroupAdminUpdate. chat-name selected-contacts) random-id))))) +(defn show-profile [identity {:keys [db] :as cofx}] + (handlers-macro/merge-fx cofx + {:db (assoc db :contacts/identity identity)} + (navigation/navigate-forget :profile))) + (handlers/register-handler-fx :show-profile [re-frame/trim-v] - (fn [{:keys [db] :as cofx} [identity]] - (handlers-macro/merge-fx cofx - {:db (assoc db :contacts/identity identity)} - (navigation/navigate-forget :profile)))) + (fn [cofx [identity]] + (show-profile identity cofx))) (handlers/register-handler-fx :resend-message diff --git a/src/status_im/ui/screens/db.cljs b/src/status_im/ui/screens/db.cljs index 0ae6d22e39..90f2a7f55e 100644 --- a/src/status_im/ui/screens/db.cljs +++ b/src/status_im/ui/screens/db.cljs @@ -150,6 +150,10 @@ (spec/def ::device-UUID (spec/nilable string?)) +;;;;UNIVERSAL LINKS + +(spec/def :universal-links/url (spec/nilable string?)) + (spec/def ::db (allowed-keys :opt [:contacts/contacts @@ -187,6 +191,7 @@ :inbox/last-received :inbox/current-id :inbox/fetching? + :universal-links/url :browser/browsers :browser/options :new/open-dapp diff --git a/src/status_im/ui/screens/events.cljs b/src/status_im/ui/screens/events.cljs index d547f318ae..426985891f 100644 --- a/src/status_im/ui/screens/events.cljs +++ b/src/status_im/ui/screens/events.cljs @@ -13,6 +13,9 @@ status-im.ui.screens.group.chat-settings.events status-im.ui.screens.group.events [status-im.ui.screens.navigation :as navigation] + [status-im.utils.universal-links.core :as universal-links] + + status-im.utils.universal-links.events status-im.ui.screens.add-new.new-chat.navigation status-im.ui.screens.network-settings.events status-im.ui.screens.profile.events @@ -315,7 +318,7 @@ (handlers/register-handler-fx :initialize-account - (fn [_ [_ address events-after]] + (fn [cofx [_ address events-after]] {:dispatch-n (cond-> [[:initialize-account-db address] [:initialize-protocol address] [:fetch-web3-node-version] @@ -329,7 +332,8 @@ [:update-transactions] [:get-fcm-token] [:update-sign-in-time] - [:show-mainnet-is-default-alert]] + [:show-mainnet-is-default-alert] + (universal-links/stored-url-event cofx)] (seq events-after) (into events-after))})) (handlers/register-handler-fx diff --git a/src/status_im/ui/screens/views.cljs b/src/status_im/ui/screens/views.cljs index fcbdb14d20..e353037589 100644 --- a/src/status_im/ui/screens/views.cljs +++ b/src/status_im/ui/screens/views.cljs @@ -2,6 +2,7 @@ (:require-macros [status-im.utils.views :refer [defview letsubs] :as views]) (:require [re-frame.core :refer [dispatch]] [status-im.utils.platform :refer [android?]] + [status-im.utils.universal-links.core :as utils.universal-links] [status-im.ui.components.react :refer [view modal create-main-screen-view] :as react] [status-im.ui.components.styles :as common-styles] [status-im.ui.screens.main-tabs.views :refer [main-tabs]] @@ -134,7 +135,9 @@ (defview main [] (letsubs [signed-up? [:signed-up?] view-id [:get :view-id]] - {:component-will-update (fn [] (react/dismiss-keyboard!))} + {:component-did-mount utils.universal-links/initialize + :component-will-unmount utils.universal-links/finalize + :component-will-update (fn [] (react/dismiss-keyboard!))} (when view-id (let [component (get-main-component view-id) main-screen-view (create-main-screen-view view-id)] diff --git a/src/status_im/utils/config.cljs b/src/status_im/utils/config.cljs index ecd07307b3..4dd65a60df 100644 --- a/src/status_im/utils/config.cljs +++ b/src/status_im/utils/config.cljs @@ -22,6 +22,7 @@ (def stub-status-go? (enabled? (get-config :STUB_STATUS_GO 0))) (def offline-inbox-enabled? (enabled? (get-config :OFFLINE_INBOX_ENABLED "1"))) (def bootnodes-settings-enabled? (enabled? (get-config :BOOTNODES_SETTINGS_ENABLED "1"))) +(def universal-links-enabled? (enabled? (get-config :UNIVERSAL_LINK_ENABLED "1"))) (def log-level (-> (get-config :LOG_LEVEL "error") string/lower-case diff --git a/src/status_im/utils/universal_links/core.cljs b/src/status_im/utils/universal_links/core.cljs new file mode 100644 index 0000000000..0aa3282c05 --- /dev/null +++ b/src/status_im/utils/universal_links/core.cljs @@ -0,0 +1,100 @@ +(ns status-im.utils.universal-links.core + (:require + [taoensso.timbre :as log] + [re-frame.core :as re-frame] + [status-im.utils.config :as config] + [status-im.chat.events :as chat.events] + [status-im.models.account :as models.account] + [status-im.ui.components.react :as react])) + +(def public-chat-regex #".*/chat/public/(.*)$") +(def profile-regex #".*/user/(.*)$") + +(defn handle-public-chat [public-chat cofx] + (log/info "universal-links: handling public chat " public-chat) + (chat.events/create-new-public-chat public-chat cofx)) + +(defn handle-view-profile [profile-id cofx] + (log/info "universal links: handling view profile" profile-id) + (chat.events/show-profile profile-id cofx)) + +(defn handle-not-found [full-url] + (log/info "universal links: no handler for " full-url)) + +(defn match-url [url regex] + (some->> url + (re-matches regex) + peek)) + +(defn stored-url-event + "Return an event description for processing a url if in the database" + [{:keys [db]}] + (when-let [url (:universal-links/url db)] + [:handle-universal-link url])) + +(defn dispatch-url + "Dispatch url so we can get access to re-frame/db" + [url] + (if-not (nil? url) + (re-frame/dispatch [:handle-universal-link url]) + (log/debug "universal links: no url"))) + +(defn store-url-for-later + "Store the url in the db to be processed on login" + [url {:keys [db]}] + (assoc-in {:db db} [:db :universal-links/url] url)) + +(defn clear-url + "Remove a url from the db" + [{:keys [db]}] + (update {:db db} :db dissoc :universal-links/url)) + +(defn route-url + "Match a url against a list of routes and handle accordingly" + [url cofx] + (cond + (match-url url public-chat-regex) + (handle-public-chat (match-url url public-chat-regex) cofx) + + (match-url url profile-regex) + (handle-view-profile (match-url url profile-regex) cofx) + + :else (handle-not-found url))) + +(defn handle-url + "Store url in the database if the user is not logged in, to be processed + on login, otherwise just handle it" + [url cofx] + (if (models.account/logged-in? cofx) + (do + (clear-url cofx) + (route-url url cofx)) + (store-url-for-later url cofx))) + +(defn unwrap-js-url [e] + (-> e + (js->clj :keywordize-keys true) + :url)) + +(def url-event-listener + (comp dispatch-url unwrap-js-url)) + +(defn initialize + "Add an event listener for handling background->foreground transition + and handles incoming url if the app has been started by clicking on a link" + [] + (when config/universal-links-enabled? + (log/debug "universal-links: initializing") + (.. react/linking + (getInitialURL) + (then dispatch-url)) + (.. react/linking + (addEventListener "url" url-event-listener)))) + +(defn finalize + "Remove event listener for url" + [] + (when config/universal-links-enabled? + (log/debug "universal-links: finalizing") + (.. react/linking + (removeEventListener "url" url-event-listener)))) diff --git a/src/status_im/utils/universal_links/events.cljs b/src/status_im/utils/universal_links/events.cljs new file mode 100644 index 0000000000..b34b72fcb4 --- /dev/null +++ b/src/status_im/utils/universal_links/events.cljs @@ -0,0 +1,14 @@ +(ns status-im.utils.universal-links.events + (:require [re-frame.core :as re-frame] + [taoensso.timbre :as log] + [status-im.utils.config :as config] + [status-im.utils.handlers :as handlers] + [status-im.utils.handlers-macro :as handlers-macro] + [status-im.utils.universal-links.core :as universal-links])) + +(handlers/register-handler-fx + :handle-universal-link + (fn [cofx [_ url]] + (log/debug "universal links: event received for " url) + (when config/universal-links-enabled? + (universal-links/handle-url url cofx)))) diff --git a/test/cljs/status_im/test/runner.cljs b/test/cljs/status_im/test/runner.cljs index 7a7edc76f2..0f123cd372 100644 --- a/test/cljs/status_im/test/runner.cljs +++ b/test/cljs/status_im/test/runner.cljs @@ -42,6 +42,7 @@ [status-im.test.utils.mixpanel] [status-im.test.utils.prices] [status-im.test.utils.keychain.core] + [status-im.test.utils.universal-links.core] [status-im.test.ui.screens.accounts.login.events])) (enable-console-print!) @@ -95,4 +96,5 @@ 'status-im.test.utils.mixpanel 'status-im.test.utils.prices 'status-im.test.utils.keychain.core + 'status-im.test.utils.universal-links.core 'status-im.test.ui.screens.accounts.login.events) diff --git a/test/cljs/status_im/test/utils/universal_links/core.cljs b/test/cljs/status_im/test/utils/universal_links/core.cljs new file mode 100644 index 0000000000..3ee21366c7 --- /dev/null +++ b/test/cljs/status_im/test/utils/universal_links/core.cljs @@ -0,0 +1,57 @@ +(ns status-im.test.utils.universal-links.core + (:require [cljs.test :refer-macros [deftest is testing]] + [re-frame.core :as re-frame] + [status-im.utils.universal-links.core :as links])) + +(deftest handle-url-test + (testing "the user is not logged in" + (testing "it stores the url for later processing" + (is (= {:db {:universal-links/url "some-url"}} + (links/handle-url "some-url" {:db {}}))))) + (testing "the user is logged in" + (let [db {:account/account {:public-key "pk"} + :universal-links/url "some-url"}] + (testing "it clears the url" + (is (nil? (get-in (links/handle-url "some-url" + {:db db}) + [:db :universal-links/url])))) + (testing "a public chat link" + (testing "it joins the chat" + (is (get-in (links/handle-url "app://get.status.im/chat/public/status" + {:db db}) + [:db :chats "status"])))) + (testing "a user profile link" + (testing "it loads the profile" + (let [actual (links/handle-url "app://get.status.im/user/profile-id" + {:db db})] + (is (= "profile-id" (get-in actual [:db :contacts/identity]))) + (is (= :profile (get-in actual [:db :view-id])))))) + (testing "a not found url" + (testing "it does nothing" + (is (nil? (links/handle-url "app://get.status.im/not-existing" + {:db db})))))))) + +(deftest url-event-listener + (testing "the url is not nil" + (testing "it dispatches the url" + (let [actual (atom nil)] + (with-redefs [re-frame/dispatch #(reset! actual %)] + (links/url-event-listener #js {:url "some-url"}) + (is (= [:handle-universal-link "some-url"] @actual)))))) + (testing "the url is nil" + (testing "it does not dispatches the url" + (let [actual (atom nil)] + (with-redefs [re-frame/dispatch #(reset! actual %)] + (links/url-event-listener #js {}) + (is (= nil @actual))))))) + +(deftest stored-url-event + (testing "the url is in the database" + (testing "it returns the event" + (= [:handle-universal-link "some-url"] + (links/stored-url-event {:db {:universal-links/url "some-url"}})))) + (testing "the url is not in the database" + (testing "it returns nil" + (= nil + (links/stored-url-event {:db {}}))))) +