diff --git a/src/status_im/contact_recovery/core.cljs b/src/status_im/contact_recovery/core.cljs new file mode 100644 index 0000000000..4b2d1747e7 --- /dev/null +++ b/src/status_im/contact_recovery/core.cljs @@ -0,0 +1,63 @@ +(ns status-im.contact-recovery.core + (:require + [status-im.i18n :as i18n] + [re-frame.core :as re-frame] + [status-im.data-store.contact-recovery :as data-store.contact-recovery] + [status-im.utils.fx :as fx] + [status-im.accounts.db :as accounts.db] + [status-im.contact.core :as models.contact])) + +(defn prompt-dismissed! [public-key] + (re-frame/dispatch [:contact-recovery.ui/prompt-dismissed public-key])) + +(defn prompt-accepted! [public-key] + (re-frame/dispatch [:contact-recovery.ui/prompt-accepted public-key])) + +(defn show-contact-recovery-fx + "Check that a pop up for that given user is not already shown, if not proceed fetching from the db whether we should be showing it" + [{:keys [db] :as cofx} public-key] + (let [my-public-key (accounts.db/current-public-key cofx) + pfs? (get-in db [:account/account :settings :pfs?])] + (when (and (not= public-key my-public-key) + pfs? + (not (get-in db [:contact-recovery/pop-up public-key]))) + {:db (update db :contact-recovery/pop-up conj public-key) + :contact-recovery/show-contact-recovery-message public-key}))) + +(fx/defn prompt-dismissed [{:keys [db]} public-key] + {:db (update db :contact-recovery/pop-up disj public-key)}) + +(defn show-contact-recovery-message? [public-key] + (not (data-store.contact-recovery/get-contact-recovery-by-id public-key))) + +(defn show-contact-recovery-message-fx [public-key] + (when (show-contact-recovery-message? public-key) + (re-frame/dispatch [:contact-recovery.callback/show-contact-recovery-message public-key]))) + +(re-frame/reg-fx + :contact-recovery/show-contact-recovery-message + show-contact-recovery-message-fx) + +(fx/defn save-contact-recovery [{:keys [now]} public-key] + {:data-store/tx [(data-store.contact-recovery/save-contact-recovery-tx {:timestamp now + :id public-key})]}) + +(fx/defn prompt-accepted [cofx public-key] + (fx/merge + cofx + (prompt-dismissed public-key) + (save-contact-recovery public-key))) + +(fx/defn show-contact-recovery-message [{:keys [db] :as cofx} public-key] + (let [pfs? (get-in db [:account/account :settings :pfs?]) + contact (models.contact/build-contact cofx public-key) + popup {:ui/show-confirmation {:title (i18n/label :t/contact-recovery-title {:name (:name contact)}) + :content (i18n/label :t/contact-recovery-content {:name (:name contact)}) + :confirm-button-text (i18n/label :t/add-to-contacts) + :cancel-button-text (i18n/label :t/cancel) + :on-cancel #(prompt-dismissed! public-key) + :on-accept #(prompt-accepted! public-key)}}] + + (when pfs? + (fx/merge cofx + popup)))) diff --git a/src/status_im/data_store/contact_recovery.cljs b/src/status_im/data_store/contact_recovery.cljs new file mode 100644 index 0000000000..743b69ec3b --- /dev/null +++ b/src/status_im/data_store/contact_recovery.cljs @@ -0,0 +1,14 @@ +(ns status-im.data-store.contact-recovery + (:require [status-im.data-store.realm.core :as core])) + +(defn get-contact-recovery-by-id [public-key] + (core/single (core/get-by-field @core/account-realm :contact-recovery :id public-key))) + +(defn save-contact-recovery-tx + "Returns tx function for saving a contact-recovery" + [contact-recovery] + (fn [realm] + (core/create realm + :contact-recovery + contact-recovery + true))) diff --git a/src/status_im/data_store/realm/schemas/account/contact_recovery.cljs b/src/status_im/data_store/realm/schemas/account/contact_recovery.cljs new file mode 100644 index 0000000000..b31546c10b --- /dev/null +++ b/src/status_im/data_store/realm/schemas/account/contact_recovery.cljs @@ -0,0 +1,7 @@ +(ns status-im.data-store.realm.schemas.account.contact-recovery) + +(def v1 {:name :contact-recovery + :primaryKey :id + :properties {:id :string + :timestamp {:type :int + :optional true}}}) diff --git a/src/status_im/data_store/realm/schemas/account/core.cljs b/src/status_im/data_store/realm/schemas/account/core.cljs index de37a794c6..f008dddd55 100644 --- a/src/status_im/data_store/realm/schemas/account/core.cljs +++ b/src/status_im/data_store/realm/schemas/account/core.cljs @@ -13,6 +13,7 @@ [status-im.data-store.realm.schemas.account.request :as request] [status-im.data-store.realm.schemas.account.membership-update :as membership-update] [status-im.data-store.realm.schemas.account.installation :as installation] + [status-im.data-store.realm.schemas.account.contact-recovery :as contact-recovery] [status-im.data-store.realm.schemas.account.migrations :as migrations] [taoensso.timbre :as log])) @@ -343,6 +344,20 @@ browser/v8 dapp-permissions/v9]) +(def v32 [chat/v13 + transport/v7 + contact/v3 + message/v9 + mailserver/v11 + mailserver-topic/v1 + user-status/v2 + membership-update/v1 + installation/v2 + local-storage/v1 + browser/v8 + dapp-permissions/v9 + contact-recovery/v1]) + ;; put schemas ordered by version (def schemas [{:schema v1 :schemaVersion 1 @@ -436,4 +451,7 @@ :migration migrations/v30} {:schema v31 :schemaVersion 31 + :migration (constantly nil)} + {:schema v32 + :schemaVersion 32 :migration (constantly nil)}]) diff --git a/src/status_im/events.cljs b/src/status_im/events.cljs index 1a83b1c23f..937bd0a611 100644 --- a/src/status_im/events.cljs +++ b/src/status_im/events.cljs @@ -16,6 +16,7 @@ [status-im.chat.models.loading :as chat.loading] [status-im.chat.models.message :as chat.message] [status-im.contact.core :as contact] + [status-im.contact-recovery.core :as contact-recovery] [status-im.data-store.core :as data-store] [status-im.extensions.core :as extensions] [status-im.extensions.registry :as extensions.registry] @@ -1404,3 +1405,25 @@ :pairing.callback/disable-installation-success (fn [cofx [_ installation-id]] (pairing/disable cofx installation-id))) + +;; Contact recovery module + +(handlers/register-handler-fx + :contact-recovery.ui/prompt-accepted + [(re-frame/inject-cofx :random-id-generator)] + (fn [cofx [_ public-key]] + (fx/merge + cofx + (contact/add-contact public-key) + (contact-recovery/prompt-accepted public-key)))) + +(handlers/register-handler-fx + :contact-recovery.ui/prompt-dismissed + (fn [cofx [_ public-key]] + (contact-recovery/prompt-dismissed cofx public-key))) + +(handlers/register-handler-fx + :contact-recovery.callback/show-contact-recovery-message + [(re-frame/inject-cofx :random-id-generator)] + (fn [cofx [_ public-key]] + (contact-recovery/show-contact-recovery-message cofx public-key))) diff --git a/src/status_im/signals/core.cljs b/src/status_im/signals/core.cljs index 92aecd898a..9aadd0d1bf 100644 --- a/src/status_im/signals/core.cljs +++ b/src/status_im/signals/core.cljs @@ -4,6 +4,7 @@ [status-im.init.core :as init] [status-im.node.core :as node] [status-im.pairing.core :as pairing] + [status-im.contact-recovery.core :as contact-recovery] [status-im.mailserver.core :as mailserver] [status-im.transport.message.core :as transport.message] [status-im.utils.fx :as fx] @@ -75,5 +76,6 @@ "mailserver.request.completed" (mailserver/handle-request-completed cofx event) "mailserver.request.expired" (when (accounts.db/logged-in? cofx) (mailserver/resend-request cofx {:request-id (:hash event)})) + "messages.decrypt.failed" (contact-recovery/show-contact-recovery-fx cofx (:sender event)) "discovery.summary" (summary cofx event) (log/debug "Event " type " not handled")))) diff --git a/src/status_im/transport/message/protocol.cljs b/src/status_im/transport/message/protocol.cljs index 1266be424b..2cc9a67df1 100644 --- a/src/status_im/transport/message/protocol.cljs +++ b/src/status_im/transport/message/protocol.cljs @@ -35,19 +35,36 @@ :resend? resend? :now now}))}) +(defn send-public-message + "Sends the payload to topic" + [{:keys [db] :as cofx} chat-id success-event payload] + (let [{:keys [web3]} db] + {:shh/send-public-message [{:web3 web3 + :success-event success-event + :src (accounts.db/current-public-key cofx) + :chat chat-id + :payload payload}]})) + (fx/defn send-with-sym-key "Sends the payload using symetric key and topic from db (looked up by `chat-id`)" [{:keys [db] :as cofx} {:keys [payload chat-id success-event]}] ;; we assume that the chat contains the contact public-key (let [{:keys [web3]} db - {:keys [sym-key-id topic]} (get-in db [:transport/chats chat-id])] - {:shh/post [{:web3 web3 - :success-event success-event - :message (merge {:sig (accounts.db/current-public-key cofx) - :symKeyID sym-key-id - :payload payload - :topic topic} - whisper-opts)}]})) + {:keys [sym-key-id topic]} (get-in db [:transport/chats chat-id]) + pfs? (get-in db [:account/account :settings :pfs?])] + (if pfs? + (send-public-message + cofx + chat-id + success-event + payload) + {:shh/post [{:web3 web3 + :success-event success-event + :message (merge {:sig (accounts.db/current-public-key cofx) + :symKeyID sym-key-id + :payload payload + :topic topic} + whisper-opts)}]}))) (fx/defn send-direct-message "Sends the payload using to dst" @@ -59,33 +76,28 @@ :dst dst :payload payload}]})) -(defn send-public-message - "Sends the payload to topic" - [{:keys [db] :as cofx} chat-id success-event payload] - (let [{:keys [web3]} db] - {:shh/send-public-message [{:web3 web3 - :success-event success-event - :src (accounts.db/current-public-key cofx) - :chat chat-id - :payload payload}]})) - (fx/defn send-with-pubkey "Sends the payload using asymetric key (account `:public-key` in db) and fixed discovery topic" [{:keys [db] :as cofx} {:keys [payload chat-id success-event]}] (let [{:keys [web3]} db] - {:shh/post [{:web3 web3 - :success-event success-event - :message (merge {:sig (accounts.db/current-public-key cofx) - :pubKey chat-id - :payload payload - :topic (transport.utils/get-topic constants/contact-discovery)} - whisper-opts)}]})) + (let [pfs? (get-in db [:account/account :settings :pfs?])] + (if pfs? + (send-direct-message cofx + chat-id + success-event + payload) + {:shh/post [{:web3 web3 + :success-event success-event + :message (merge {:sig (accounts.db/current-public-key cofx) + :pubKey chat-id + :payload payload + :topic (transport.utils/get-topic constants/contact-discovery)} + whisper-opts)}]})))) (defrecord Message [content content-type message-type clock-value timestamp] StatusMessage (send [this chat-id {:keys [message-id] :as cofx}] (let [dev-mode? (get-in cofx [:db :account/account :dev-mode?]) - pfs? (get-in cofx [:db :account/account :settings :pfs?]) current-public-key (accounts.db/current-public-key cofx) params {:chat-id chat-id :payload this @@ -95,25 +107,13 @@ message-type]}] (case message-type :public-group-user-message - (if pfs? - (send-public-message - cofx - chat-id - (:success-event params) - this) - (send-with-sym-key cofx params)) + (send-with-sym-key cofx params) :user-message - (if pfs? - (send-direct-message - cofx - chat-id - (:success-event params) - this) - (fx/merge cofx - #(when (config/pairing-enabled? dev-mode?) - (send-direct-message % current-public-key nil this)) - (send-with-pubkey params)))))) + (fx/merge cofx + #(when (config/pairing-enabled? dev-mode?) + (send-direct-message % current-public-key nil this)) + (send-with-pubkey params))))) (receive [this chat-id signature _ cofx] {:chat-received-message/add-fx [(assoc (into {} this) @@ -134,14 +134,8 @@ (defrecord MessagesSeen [message-ids] StatusMessage (send [this chat-id cofx] - (let [pfs? (get-in cofx [:db :account/account :settings :pfs?])] - (if pfs? - (send-direct-message cofx - chat-id - nil - this) - (send-with-pubkey cofx {:chat-id chat-id - :payload this})))) + (send-with-pubkey cofx {:chat-id chat-id + :payload this})) (receive [this chat-id signature _ cofx] (chat/receive-seen cofx chat-id signature this)) (validate [this] diff --git a/src/status_im/ui/screens/db.cljs b/src/status_im/ui/screens/db.cljs index dee8cb0e0e..a722efd3f6 100644 --- a/src/status_im/ui/screens/db.cljs +++ b/src/status_im/ui/screens/db.cljs @@ -24,6 +24,7 @@ :navigation-stack '() :contacts/contacts {} :pairing/installations {} + :contact-recovery/pop-up #{} :qr-codes {} :group/selected-contacts #{} :chats {} diff --git a/test/cljs/status_im/test/contact-recovery/core.cljs b/test/cljs/status_im/test/contact-recovery/core.cljs new file mode 100644 index 0000000000..76e1c9667a --- /dev/null +++ b/test/cljs/status_im/test/contact-recovery/core.cljs @@ -0,0 +1,38 @@ +(ns status-im.test.contact-recovery.core + (:require [cljs.test :refer-macros [deftest is testing]] + [status-im.contact-recovery.core :as contact-recovery])) + +(deftest show-contact-recovery-fx + (let [public-key "pk"] + (testing "pfs is not enabled" + (testing "no pop up is displayed" + (let [actual (contact-recovery/show-contact-recovery-fx {:db {:contact-recovery/pop-up #{}}} public-key)] + (testing "it does nothing" + (is (not (:db actual))) + (is (not (:contact-recovery/show-contact-recovery-message actual))))))) + (testing "no pop up is displayed" + (let [cofx {:db {:contact-recovery/pop-up #{} + :account/account {:settings {:pfs? true}}}} + actual (contact-recovery/show-contact-recovery-fx cofx public-key)] + (testing "it sets the pop up as displayed" + (is (get-in actual [:db :contact-recovery/pop-up public-key]))) + (testing "it adds an fx for fetching the contact" + (is (= public-key (:contact-recovery/show-contact-recovery-message actual)))))) + (testing "pop up is already displayed" + (let [actual (contact-recovery/show-contact-recovery-fx {:db {:contact-recovery/pop-up #{public-key}}} public-key)] + (testing "it does nothing" + (is (not (:db actual))) + (is (not (:contact-recovery/show-contact-recovery-message actual)))))))) + +(deftest show-contact-recovery-message + (let [public-key "pk"] + (testing "pfs is enabled" + (let [cofx {:db {:account/account {:settings {:pfs? true}}}} + actual (contact-recovery/show-contact-recovery-message cofx public-key)] + (testing "it shows a pop up" + (is (:ui/show-confirmation actual))))) + (testing "pfs is not enabled" + (let [cofx {:db {:account/account {:settings {}}}} + actual (contact-recovery/show-contact-recovery-message cofx public-key)] + (testing "it shows a pop up" + (is (not (:ui/show-confirmation actual)))))))) diff --git a/test/cljs/status_im/test/runner.cljs b/test/cljs/status_im/test/runner.cljs index 4ff314dd0b..c6505102fd 100644 --- a/test/cljs/status_im/test/runner.cljs +++ b/test/cljs/status_im/test/runner.cljs @@ -56,6 +56,7 @@ [status-im.test.utils.fx] [status-im.test.accounts.recover.core] [status-im.test.hardwallet.core] + [status-im.test.contact-recovery.core] [status-im.test.ui.screens.currency-settings.models] [status-im.test.ui.screens.wallet.db])) @@ -124,5 +125,6 @@ 'status-im.test.ui.screens.currency-settings.models 'status-im.test.ui.screens.wallet.db 'status-im.test.browser.core + 'status-im.test.contact-recovery.core 'status-im.test.extensions.ethereum 'status-im.test.browser.permissions) diff --git a/translations/en.json b/translations/en.json index a11574344b..263a58de6b 100644 --- a/translations/en.json +++ b/translations/en.json @@ -15,6 +15,8 @@ "sync-all-devices": "Sync all devices", "syncing-devices": "Syncing...", "paired-devices": "Paired devices", + "contact-recovery-title": "{{name}} has sent you a message", + "contact-recovery-content": "{{name}} has sent you a message but did not include this device.\nThis might happen if you have more than 3 devices, you haven't paired your devices correctly or you just recovered your account.\nPlease make sure your devices are paired correctly and click Add to contacts to notify the user of this device.", "pairing-maximum-number-reached-title": "Max number of devices reached", "pairing-maximum-number-reached-content": "Please disable one of your devices before enabling a new one.", "pairing-new-installation-detected-title": "New device detected",