From 5a69b4198e8118b5a8c998239c1bca3c4756120d Mon Sep 17 00:00:00 2001 From: Pedro Pombeiro Date: Tue, 20 Nov 2018 19:36:11 +0100 Subject: [PATCH] Update PNs to use data-only messaging, and only encode/decode data values. Fixes #6772 Fix navigation to chat when PN is tapped while signed off. Fixes #3488 Anonymize PN pubkeys. Part of #6772 --- .gitignore | 1 + Makefile | 2 + STATUS_GO_VERSION | 2 +- android/app/proguard-rules.pro | 4 + ci/Jenkinsfile.ios | 2 +- doc/codebase-structure-and-guidelines.md | 33 +- .../desktop/desktopnotification.cpp | 6 +- .../desktop/desktopnotification.h | 2 +- .../status/ethereum/module/StatusModule.java | 6 +- .../react-native-status/desktop/rctstatus.cpp | 8 +- .../react-native-status/desktop/rctstatus.h | 2 +- .../ios/RCTStatus/RCTStatus.m | 5 +- src/status_im/android/core.cljs | 4 +- src/status_im/chat/models/message.cljs | 45 +-- src/status_im/core.cljs | 6 +- src/status_im/events.cljs | 11 +- src/status_im/hardwallet/card.cljs | 3 +- src/status_im/init/core.cljs | 80 ++-- src/status_im/ios/core.cljs | 3 +- src/status_im/native_module/impl/module.cljs | 4 +- src/status_im/notifications/background.cljs | 29 ++ src/status_im/notifications/core.cljs | 369 ++++++++++++++---- src/status_im/transport/message/core.cljs | 2 +- src/status_im/utils/config.cljs | 1 - src/status_im/utils/universal_links/core.cljs | 4 +- .../status_im/test/notifications/core.cljs | 43 +- 26 files changed, 462 insertions(+), 215 deletions(-) create mode 100644 src/status_im/notifications/background.cljs diff --git a/.gitignore b/.gitignore index 1d64ea7aea..0d53f333b4 100644 --- a/.gitignore +++ b/.gitignore @@ -156,3 +156,4 @@ conan*.txt conanbuildinfo.* conan.cmake /yarn-error.log +/default.realm/ diff --git a/Makefile b/Makefile index 2d1a1335e4..c5bfef0952 100644 --- a/Makefile +++ b/Makefile @@ -211,6 +211,8 @@ android-ports: ##@other Add proxies to Android Device/Simulator adb reverse tcp:4567 tcp:4567 adb forward tcp:5561 tcp:5561 +android-logcat: + adb logcat | grep -e StatusModule -e ReactNativeJS -e StatusNativeLogs startdev-%: $(eval SYSTEM := $(word 2, $(subst -, , $@))) diff --git a/STATUS_GO_VERSION b/STATUS_GO_VERSION index f1639633a9..e5a84ccee9 100644 --- a/STATUS_GO_VERSION +++ b/STATUS_GO_VERSION @@ -1 +1 @@ -181221-204011-e80de6 +0.19.0-beta.1 diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index 6e8516c8d6..123ec21672 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -68,3 +68,7 @@ -dontwarn java.nio.file.* -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement -dontwarn okio.** + +# Firebase +-keep class io.invertase.firebase.** { *; } +-dontwarn io.invertase.firebase.** diff --git a/ci/Jenkinsfile.ios b/ci/Jenkinsfile.ios index 3fa1c45ae1..005b39beb0 100644 --- a/ci/Jenkinsfile.ios +++ b/ci/Jenkinsfile.ios @@ -13,7 +13,7 @@ pipeline { timestamps() disableConcurrentBuilds() /* Prevent Jenkins jobs from running forever */ - timeout(time: 35, unit: 'MINUTES') + timeout(time: 45, unit: 'MINUTES') /* Limit builds retained */ buildDiscarder(logRotator( numToKeepStr: '10', diff --git a/doc/codebase-structure-and-guidelines.md b/doc/codebase-structure-and-guidelines.md index 8e43871a9e..5dddafd9de 100644 --- a/doc/codebase-structure-and-guidelines.md +++ b/doc/codebase-structure-and-guidelines.md @@ -6,7 +6,7 @@ Each business logic module is managed in a module directory. *There is no rigid structure on how to organize code inside modules outside of core and db namespaces* -``` +```txt - events.cljs - subs.cljs - notifications @@ -37,13 +37,13 @@ Core namespace must only contain functions that can be called outside of the mod - fx producing functions called by events and other modules -```clojure -(def get-current-account - module.db/get-current-account) + ```clojure + (def get-current-account + module.db/get-current-account) -(defn set-current-account [{db :db :as cofx}] - {:db (module.db/set-current-account db)}) -``` + (defn set-current-account [{db :db :as cofx}] + {:db (module.db/set-current-account db)}) + ``` ## db.cljs @@ -61,13 +61,14 @@ These guidelines make db.cljs namespaces the place to go when making changes to - events must always be declared with `register-handler-fx`, no `register-handler-db` - events must never use the `trim-v` interceptor - events must only contain a function call defined in a module -```clojure -(handlers/register-handler-fx - :notifications/handle-push-notification - (fn [cofx [_ event]] - (notifications/handle-push-notification event cofx))) -``` + ```clojure + (handlers/register-handler-fx + :notifications/handle-push-notification-open + (fn [cofx [_ event]] + (notifications/handle-push-notification-open event cofx))) + ``` - events must use synthetic namespaces: - - `:module.ui/` for user triggered events - - `:module.callback/` for callback events, which are events bringing back the result of an fx to the event loop, the name of the event should end with `-success` or `-error` most of the time. Other possibilities can be `-granted`, `-denied` for instance. - - `:module/` for internal events, examples are time based events marked `-timed-out`, external changes marked `-changed` or reception of external events marked `-received`. + + - `:module.ui/` for user triggered events + - `:module.callback/` for callback events, which are events bringing back the result of an fx to the event loop, the name of the event should end with `-success` or `-error` most of the time. Other possibilities can be `-granted`, `-denied` for instance. + - `:module/` for internal events, examples are time based events marked `-timed-out`, external changes marked `-changed` or reception of external events marked `-received`. diff --git a/modules/react-native-desktop-notification/desktop/desktopnotification.cpp b/modules/react-native-desktop-notification/desktop/desktopnotification.cpp index 1b7be25e47..a2d2984841 100644 --- a/modules/react-native-desktop-notification/desktop/desktopnotification.cpp +++ b/modules/react-native-desktop-notification/desktop/desktopnotification.cpp @@ -105,12 +105,12 @@ QList DesktopNotification::methodsToExport() { QVariantMap DesktopNotification::constantsToExport() { return QVariantMap(); } -void DesktopNotification::sendNotification(QString title, QString body, bool prioritary) { +void DesktopNotification::displayNotification(QString title, QString body, bool prioritary) { Q_D(DesktopNotification); - qCDebug(NOTIFICATION) << "::sendNotification"; + qCDebug(NOTIFICATION) << "::displayNotification"; if (m_appHasFocus) { - qCDebug(NOTIFICATION) << "Not sending notification since an application window is active"; + qCDebug(NOTIFICATION) << "Not displaying notification since an application window is active"; return; } diff --git a/modules/react-native-desktop-notification/desktop/desktopnotification.h b/modules/react-native-desktop-notification/desktop/desktopnotification.h index 2087869b8e..ae928256b7 100644 --- a/modules/react-native-desktop-notification/desktop/desktopnotification.h +++ b/modules/react-native-desktop-notification/desktop/desktopnotification.h @@ -35,7 +35,7 @@ public: QList methodsToExport() override; QVariantMap constantsToExport() override; - Q_INVOKABLE void sendNotification(QString title, QString body, bool prioritary); + Q_INVOKABLE void displayNotification(QString title, QString body, bool prioritary); Q_INVOKABLE void setDockBadgeLabel(const QString label); private: QScopedPointer d_ptr; diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java index f081356d37..3727c43353 100644 --- a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java @@ -81,7 +81,7 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL } @Override - public void onHostResume() { // Actvity `onResume` + public void onHostResume() { // Activity `onResume` module = this; Activity currentActivity = getCurrentActivity(); if (currentActivity == null) { @@ -459,7 +459,7 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL } @ReactMethod - public void notifyUsers(final String message, final String payloadJSON, final String tokensJSON, final Callback callback) { + public void notifyUsers(final String dataPayloadJSON, final String tokensJSON, final Callback callback) { Log.d(TAG, "notifyUsers"); if (!checkAvailability()) { callback.invoke(false); @@ -469,7 +469,7 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL Runnable r = new Runnable() { @Override public void run() { - String res = Statusgo.NotifyUsers(message, payloadJSON, tokensJSON); + String res = Statusgo.NotifyUsers(dataPayloadJSON, tokensJSON); callback.invoke(res); } diff --git a/modules/react-native-status/desktop/rctstatus.cpp b/modules/react-native-status/desktop/rctstatus.cpp index 78b11ee8f5..f4ee8060f8 100644 --- a/modules/react-native-status/desktop/rctstatus.cpp +++ b/modules/react-native-status/desktop/rctstatus.cpp @@ -136,14 +136,14 @@ void RCTStatus::createAccount(QString password, double callbackId) { } -void RCTStatus::notifyUsers(QString token, QString payloadJSON, QString tokensJSON, double callbackId) { +void RCTStatus::notifyUsers(QString dataPayloadJSON, QString tokensJSON, double callbackId) { Q_D(RCTStatus); qCDebug(RCTSTATUS) << "::notifyUsers call - callbackId:" << callbackId; - QtConcurrent::run([&](QString token, QString payloadJSON, QString tokensJSON, double callbackId) { - const char* result = NotifyUsers(token.toUtf8().data(), payloadJSON.toUtf8().data(), tokensJSON.toUtf8().data()); + QtConcurrent::run([&](QString dataPayloadJSON, QString tokensJSON, double callbackId) { + const char* result = NotifyUsers(dataPayloadJSON.toUtf8().data(), tokensJSON.toUtf8().data()); logStatusGoResult("::notifyUsers Notify", result); d->bridge->invokePromiseCallback(callbackId, QVariantList{result}); - }, token, payloadJSON, tokensJSON, callbackId); + }, dataPayloadJSON, tokensJSON, callbackId); } diff --git a/modules/react-native-status/desktop/rctstatus.h b/modules/react-native-status/desktop/rctstatus.h index 3713fe4e98..62ad2db899 100644 --- a/modules/react-native-status/desktop/rctstatus.h +++ b/modules/react-native-status/desktop/rctstatus.h @@ -38,7 +38,7 @@ public: Q_INVOKABLE void startNode(QString configString); Q_INVOKABLE void stopNode(); Q_INVOKABLE void createAccount(QString password, double callbackId); - Q_INVOKABLE void notifyUsers(QString token, QString payloadJSON, QString tokensJSON, double callbackId); + Q_INVOKABLE void notifyUsers(QString dataPayloadJSON, QString tokensJSON, double callbackId); Q_INVOKABLE void sendLogs(QString dbJSON); Q_INVOKABLE void addPeer(QString enode, double callbackId); Q_INVOKABLE void recoverAccount(QString passphrase, QString password, double callbackId); diff --git a/modules/react-native-status/ios/RCTStatus/RCTStatus.m b/modules/react-native-status/ios/RCTStatus/RCTStatus.m index a5c6d308ed..f6df1e8eef 100644 --- a/modules/react-native-status/ios/RCTStatus/RCTStatus.m +++ b/modules/react-native-status/ios/RCTStatus/RCTStatus.m @@ -185,11 +185,10 @@ RCT_EXPORT_METHOD(createAccount:(NSString *)password //////////////////////////////////////////////////////////////////// #pragma mark - NotifyUsers method //////////////////////////////////////////////////////////////////// notifyUsers -RCT_EXPORT_METHOD(notifyUsers:(NSString *)message - payloadJSON:(NSString *)payloadJSON +RCT_EXPORT_METHOD(notifyUsers:(NSString *)dataPayloadJSON tokensJSON:(NSString *)tokensJSON callback:(RCTResponseSenderBlock)callback) { - char * result = NotifyUsers((char *) [message UTF8String], (char *) [payloadJSON UTF8String], (char *) [tokensJSON UTF8String]); + char * result = NotifyUsers((char *) [dataPayloadJSON UTF8String], (char *) [tokensJSON UTF8String]); callback(@[[NSString stringWithUTF8String: result]]); #if DEBUG NSLog(@"NotifyUsers() method called"); diff --git a/src/status_im/android/core.cljs b/src/status_im/android/core.cljs index 6cb0ca5885..b44c2d8724 100644 --- a/src/status_im/android/core.cljs +++ b/src/status_im/android/core.cljs @@ -40,9 +40,7 @@ (.addEventListener react/app-state "change" app-state-change-handler)) :component-did-mount (fn [this] - (dispatch [:set-initial-props (reagent/props this)]) - ;; TODO(oskarth): Background click_action handler - (notifications/init)) + (dispatch [:set-initial-props (reagent/props this)])) :component-will-unmount (fn [] (.stop react/http-bridge) diff --git a/src/status_im/chat/models/message.cljs b/src/status_im/chat/models/message.cljs index f17c0c8253..e1f2bf605e 100644 --- a/src/status_im/chat/models/message.cljs +++ b/src/status_im/chat/models/message.cljs @@ -121,7 +121,7 @@ (get-in db [:account/account :desktop-notifications?]) (< (time/seconds-ago (time/to-date timestamp)) constants/one-earth-day)) (let [{:keys [title body prioritary?]} (build-desktop-notification cofx message)] - (.sendNotification react/desktop-notification title body prioritary?))) + (.displayNotification react/desktop-notification title body prioritary?))) (fx/merge cofx {:db (cond-> (-> db @@ -159,19 +159,6 @@ message (assoc message :clock-value (utils.clocks/send last-clock-value)))) -(fx/defn display-notification - [cofx chat-id] - (when config/in-app-notifications-enabled? - (let [view-id (get-in cofx [:db :view-id]) - from (accounts.db/current-public-key cofx) - current-chat-id (get-in cofx [:db :current-chat-id])] - (when-not (and (= :chat view-id) - (= current-chat-id chat-id)) - {:notifications/display-notification {:title (i18n/label :notifications-new-message-title) - :body (i18n/label :notifications-new-message-body) - :to chat-id - :from from}})))) - (defn check-response-to [{{:keys [response-to response-to-v2]} :content :as message} old-id->message] @@ -210,7 +197,6 @@ current-chat? :seen :else :received)) (commands-receiving/receive message) - (display-notification chat-id) (send-message-seen chat-id message-id (and (not group-chat) current-chat? (not (= constants/system from)) @@ -391,14 +377,14 @@ (add-own-status chat-id message-id :sending) (send chat-id message-id wrapped-record)))) -(fx/defn send-push-notification [cofx fcm-token status] +(fx/defn send-push-notification [cofx message-id fcm-token status] + (log/debug "#6772 - send-push-notification" message-id fcm-token) (when (and fcm-token (= status :sent)) - {:send-notification {:message (js/JSON.stringify #js {:from (accounts.db/current-public-key cofx) - :to (get-in cofx [:db :current-chat-id])}) - :payload {:title (i18n/label :notifications-new-message-title) - :body (i18n/label :notifications-new-message-body) - :sound notifications/sound-name} - :tokens [fcm-token]}})) + (let [payload {:from (accounts.db/current-public-key cofx) + :to (get-in cofx [:db :current-chat-id]) + :id message-id}] + {:send-notification {:data-payload (notifications/encode-notification-payload payload) + :tokens [fcm-token]}}))) (fx/defn update-message-status [{:keys [db]} chat-id message-id status] (let [from (get-in db [:chats chat-id :messages message-id :from]) @@ -484,8 +470,13 @@ (re-frame/reg-fx :send-notification - (fn [{:keys [message payload tokens]}] - (let [payload-json (types/clj->json payload) - tokens-json (types/clj->json tokens)] - (log/debug "send-notification message: " message " payload-json: " payload-json " tokens-json: " tokens-json) - (status/notify-users {:message message :payload payload-json :tokens tokens-json} #(log/debug "send-notification cb result: " %))))) + (fn [{:keys [data-payload tokens]}] + "Sends a notification to another device. data-payload is a Clojure map of strings to strings" + (let [data-payload-json (types/clj->json data-payload) + tokens-json (types/clj->json tokens)] + (log/debug "send-notification data-payload-json:" data-payload-json "tokens-json:" tokens-json) + ;; NOTE: react-native-firebase doesn't have a good implementation of sendMessage + ;; (supporting e.g. priority or content_available properties), + ;; therefore we must use an implementation in status-go. + (status/notify-users {:data-payload data-payload-json :tokens tokens-json} + #(log/debug "send-notification cb result: " %))))) diff --git a/src/status_im/core.cljs b/src/status_im/core.cljs index f9d03daa98..3ee39b96b6 100644 --- a/src/status_im/core.cljs +++ b/src/status_im/core.cljs @@ -1,7 +1,9 @@ (ns status-im.core (:require [re-frame.core :as re-frame] [status-im.utils.error-handler :as error-handler] + [status-im.utils.platform :as platform] [status-im.ui.components.react :as react] + [status-im.notifications.background :as background-messaging] [reagent.core :as reagent] status-im.transport.impl.receive status-im.transport.impl.send @@ -18,4 +20,6 @@ (log/set-level! config/log-level) (error-handler/register-exception-handler!) (re-frame/dispatch [:init/app-started]) - (.registerComponent react/app-registry "StatusIm" #(reagent/reactify-component app-root))) + (.registerComponent react/app-registry "StatusIm" #(reagent/reactify-component app-root)) + (when platform/android? + (.registerHeadlessTask react/app-registry "RNFirebaseBackgroundMessage" background-messaging/message-handler-fn))) diff --git a/src/status_im/events.cljs b/src/status_im/events.cljs index 7e1aafba9d..d7b6f4e247 100644 --- a/src/status_im/events.cljs +++ b/src/status_im/events.cljs @@ -830,9 +830,9 @@ ;; notifications module (handlers/register-handler-fx - :notifications/notification-event-received - (fn [cofx [_ event]] - (notifications/handle-push-notification cofx event))) + :notifications/notification-open-event-received + (fn [cofx [_ decoded-payload ctx]] + (notifications/handle-push-notification-open cofx decoded-payload ctx))) (handlers/register-handler-fx :notifications.callback/get-fcm-token-success @@ -849,6 +849,11 @@ (fn [cofx _] (accounts/show-mainnet-is-default-alert cofx))) +(handlers/register-handler-fx + :notifications.callback/on-message + (fn [cofx [_ decoded-payload opts]] + (notifications/handle-on-message cofx decoded-payload opts))) + ;; hardwallet module (handlers/register-handler-fx diff --git a/src/status_im/hardwallet/card.cljs b/src/status_im/hardwallet/card.cljs index fc655c40cd..8ef9290476 100644 --- a/src/status_im/hardwallet/card.cljs +++ b/src/status_im/hardwallet/card.cljs @@ -19,7 +19,8 @@ (then #(re-frame/dispatch [:hardwallet.callback/check-nfc-support-success %]))))) (defn check-nfc-enabled [] - (when platform/android? + (when (and platform/android? + config/hardwallet-enabled?) (.. keycard nfcIsEnabled (then #(re-frame/dispatch [:hardwallet.callback/check-nfc-enabled-success %]))))) diff --git a/src/status_im/init/core.cljs b/src/status_im/init/core.cljs index 8c356d5b18..dd0b9b4dfd 100644 --- a/src/status_im/init/core.cljs +++ b/src/status_im/init/core.cljs @@ -74,23 +74,24 @@ (fx/defn start-app [cofx] (fx/merge cofx - {:init/get-device-UUID nil - :init/restore-native-settings nil - :ui/listen-to-window-dimensions-change nil - :notifications/handle-initial-push-notification nil - :network/listen-to-network-status nil - :network/listen-to-connection-status nil - :hardwallet/check-nfc-support nil - :hardwallet/check-nfc-enabled nil - :hardwallet/start-module nil - :hardwallet/register-card-events nil} + {:init/get-device-UUID nil + :init/restore-native-settings nil + :ui/listen-to-window-dimensions-change nil + :notifications/init nil + :network/listen-to-network-status nil + :network/listen-to-connection-status nil + :hardwallet/check-nfc-support nil + :hardwallet/check-nfc-enabled nil + :hardwallet/start-module nil + :hardwallet/register-card-events nil} (initialize-keychain))) (fx/defn initialize-app-db "Initialize db to initial state" [{{:keys [status-module-initialized? view-id hardwallet initial-props desktop/desktop - network-status network peers-count peers-summary device-UUID] + network-status network peers-count peers-summary device-UUID + push-notifications/stored] :node/keys [status] :or {network (get app-db :network)}} :db}] {:db (assoc app-db @@ -105,7 +106,8 @@ :network network :hardwallet hardwallet :device-UUID device-UUID - :view-id view-id)}) + :view-id view-id + :push-notifications/stored stored)}) (fx/defn initialize-app [cofx encryption-key] @@ -140,13 +142,18 @@ (let [{{:accounts/keys [accounts] :as db} :db} cofx] (if (empty? accounts) (navigation/navigate-to-clean cofx :intro nil) - (let [account-with-notification (first (keys (:push-notifications/stored db))) - selection-fn (if (not-empty account-with-notification) - #(filter (fn [account] - (= account-with-notification - (:public-key account))) - %) - #(sort-by :last-sign-in > %)) + (let [account-with-notification + (when-not platform/desktop? + (notifications/lookup-contact-pubkey-from-hash + cofx + (first (keys (:push-notifications/stored db))))) + selection-fn + (if (not-empty account-with-notification) + #(filter (fn [account] + (= account-with-notification + (:public-key account))) + %) + #(sort-by :last-sign-in > %)) {:keys [address photo-path name]} (first (selection-fn (vals accounts)))] (accounts.login/open-login cofx address photo-path name))))) @@ -193,12 +200,12 @@ (= view-id :create-account) (assoc-in [:accounts/create :step] :enter-name))})) -(defn login-only-events [cofx address] +(defn login-only-events [cofx address stored-pns] (fx/merge cofx {:notifications/request-notifications-permissions nil} (navigation/navigate-to-cofx :home nil) (universal-links/process-stored-event) - (notifications/process-stored-event address) + (notifications/process-stored-event address stored-pns) (when platform/desktop? (chat-model/update-dock-badge-label)))) @@ -213,22 +220,23 @@ (= (get-in cofx [:db :view-id]) :hardwallet-success)) -(fx/defn initialize-account [cofx address] - (fx/merge cofx - {:notifications/get-fcm-token nil} - (initialize-account-db address) - (contact/load-contacts) - (pairing/load-installations) - #(when (dev-mode? %) - (models.dev-server/start)) - (browser/initialize-browsers) +(fx/defn initialize-account [{:keys [db] :as cofx} address] + (let [stored-pns (:push-notifications/stored db)] + (fx/merge cofx + {:notifications/get-fcm-token nil} + (initialize-account-db address) + (contact/load-contacts) + (pairing/load-installations) + #(when (dev-mode? %) + (models.dev-server/start)) + (browser/initialize-browsers) - (browser/initialize-dapp-permissions) - (extensions.registry/initialize) - (accounts.update/update-sign-in-time) - #(when-not (or (creating-account? %) - (finishing-hardwallet-setup? %)) - (login-only-events % address)))) + (browser/initialize-dapp-permissions) + (extensions.registry/initialize) + (accounts.update/update-sign-in-time) + #(when-not (or (creating-account? %) + (finishing-hardwallet-setup? %)) + (login-only-events % address stored-pns))))) (re-frame/reg-fx :init/init-store diff --git a/src/status_im/ios/core.cljs b/src/status_im/ios/core.cljs index d2a65754c1..64ff991d24 100644 --- a/src/status_im/ios/core.cljs +++ b/src/status_im/ios/core.cljs @@ -35,8 +35,7 @@ (.addEventListener react/app-state "change" app-state-change-handler)) :component-did-mount (fn [this] - (dispatch [:set-initial-props (reagent/props this)]) - (notifications/init)) + (dispatch [:set-initial-props (reagent/props this)])) :component-will-unmount (fn [] (.stop react/http-bridge) diff --git a/src/status_im/native_module/impl/module.cljs b/src/status_im/native_module/impl/module.cljs index 29b65e87be..e629ad62e0 100644 --- a/src/status_im/native_module/impl/module.cljs +++ b/src/status_im/native_module/impl/module.cljs @@ -84,9 +84,9 @@ true) false)))))) -(defn notify-users [{:keys [message payload tokens] :as m} on-result] +(defn notify-users [{:keys [data-payload tokens] :as m} on-result] (when status - (call-module #(.notifyUsers status message payload tokens on-result)))) + (call-module #(.notifyUsers status data-payload tokens on-result)))) (defn send-logs [dbJson] (when status diff --git a/src/status_im/notifications/background.cljs b/src/status_im/notifications/background.cljs new file mode 100644 index 0000000000..cbb8facd44 --- /dev/null +++ b/src/status_im/notifications/background.cljs @@ -0,0 +1,29 @@ +(ns status-im.notifications.background + (:require [goog.object :as object] + [re-frame.core :as re-frame] + [status-im.react-native.js-dependencies :as rn] + [status-im.notifications.core :as notifications] + [status-im.i18n :as i18n] + [cljs.core.async :as async] + [taoensso.timbre :as log] + [status-im.utils.platform :as platform])) + +(when-not platform/desktop? + (def firebase (object/get rn/react-native-firebase "default"))) + +(defn message-handler-fn [] + ;; message-js is firebase.messaging.RemoteMessage: https://github.com/invertase/react-native-firebase-docs/blob/master/docs/messaging/reference/RemoteMessage.md + (fn [message-js] + (js/Promise. + (fn [on-success on-error] + (try + (when message-js + (log/debug "message-handler-fn called" (js/JSON.stringify message-js)) + (let [decoded-payload (notifications/decode-notification-payload message-js)] + (when decoded-payload + (log/debug "dispatching :notifications.callback/on-message to display background message" decoded-payload) + (re-frame/dispatch [:notifications.callback/on-message decoded-payload {:force true}])))) + (on-success) + (catch :default e + (log/warn "failed to handle background message" e) + (on-error e))))))) diff --git a/src/status_im/notifications/core.cljs b/src/status_im/notifications/core.cljs index 29f3b51d2e..a9793c0d0b 100644 --- a/src/status_im/notifications/core.cljs +++ b/src/status_im/notifications/core.cljs @@ -2,15 +2,22 @@ (:require [goog.object :as object] [re-frame.core :as re-frame] [status-im.react-native.js-dependencies :as rn] + [status-im.js-dependencies :as dependencies] [taoensso.timbre :as log] + [status-im.i18n :as i18n] [status-im.accounts.db :as accounts.db] [status-im.chat.models :as chat-model] [status-im.utils.platform :as platform] - [status-im.utils.fx :as fx])) + [status-im.utils.fx :as fx] + [status-im.utils.utils :as utils])) ;; Work in progress namespace responsible for push notifications and interacting ;; with Firebase Cloud Messaging. +(def ^:private pn-message-id-hash-length 10) +(def ^:private pn-pubkey-hash-length 10) +(def ^:private pn-pubkey-length 132) + (when-not platform/desktop? (def firebase (object/get rn/react-native-firebase "default"))) @@ -28,42 +35,157 @@ (log/debug "notifications-denied") (re-frame/dispatch [:notifications.callback/request-notifications-permissions-denied {}])))))) +(defn valid-notification-payload? + [{:keys [from to]}] + (and from to + (or + ;; is it full pubkey? + (and (= (.-length from) pn-pubkey-length) + (= (.-length to) pn-pubkey-length)) + ;; partially deanonymized + (and (= (.-length from) pn-pubkey-hash-length) + (= (.-length to) pn-pubkey-length)) + ;; or is it an anonymized pubkey hash (v2 payload)? + (and (= (.-length from) pn-pubkey-hash-length) + (= (.-length to) pn-pubkey-hash-length))))) + +(defn sha3 [s] + (.sha3 dependencies/Web3.prototype s)) + +(defn anonymize-pubkey + [pubkey] + "Anonymize a public key, if needed, by hashing it and taking the first 4 bytes" + (if (= (count pubkey) pn-pubkey-hash-length) + pubkey + (apply str (take pn-pubkey-hash-length (sha3 pubkey))))) + +(defn encode-notification-payload + [{:keys [from to id] :as payload}] + (if (valid-notification-payload? payload) + {:msg-v2 (js/JSON.stringify #js {:from (anonymize-pubkey from) + :to (anonymize-pubkey to) + :id (apply str (take pn-message-id-hash-length id))})} + (throw (str "Invalid push notification payload" payload)))) + +(when platform/desktop? + (defn handle-initial-push-notification [] ())) ;; no-op + (when-not platform/desktop? - (defn get-fcm-token [] - (-> (.getToken (.messaging firebase)) - (.then (fn [x] - (log/debug "get-fcm-token: " x) - (re-frame/dispatch [:notifications.callback/get-fcm-token-success x]))))) - - (defn on-refresh-fcm-token [] - (.onTokenRefresh (.messaging firebase) - (fn [x] - (log/debug "on-refresh-fcm-token: " x) - (re-frame/dispatch [:notifications.callback/get-fcm-token-success x])))) - - ;; TODO(oskarth): Only called in background on iOS right now. - ;; NOTE(oskarth): Hardcoded data keys :sum and :msg in status-go right now. - (defn on-notification [] - (.onNotification (.notifications firebase) - (fn [event-js] - (let [event (js->clj event-js :keywordize-keys true) - data (select-keys event [:sum :msg]) - aps (:aps event)] - (log/debug "on-notification event: " (pr-str event)) - (log/debug "on-notification aps: " (pr-str aps)) - (log/debug "on-notification data: " (pr-str data)))))) - (def channel-id "status-im") (def channel-name "Status") (def sound-name "message.wav") (def group-id "im.status.ethereum.MESSAGE") (def icon "ic_stat_status_notification") + (defn- hash->pubkey [hash accounts] + (:public-key + (first + (filter #(= (anonymize-pubkey (:public-key %)) hash) + (vals accounts))))) + + (defn lookup-contact-pubkey-from-hash + [{:keys [db] :as cofx} contact-pubkey-or-hash] + "Tries to deanonymize a given contact pubkey hash by looking up the + full pubkey (if db is unlocked) in :contacts/contacts. + Returns original value if not a hash (e.g. already a public key)." + (if (and contact-pubkey-or-hash + (= (count contact-pubkey-or-hash) pn-pubkey-hash-length)) + (if-let + [account-pubkey (hash->pubkey contact-pubkey-or-hash + (:accounts/accounts db))] + account-pubkey + (if (accounts.db/logged-in? cofx) + ;; TODO: for simplicity we're doing a linear lookup of the contacts, + ;; but we might want to build a map of hashed pubkeys to pubkeys + ;; for this purpose + (hash->pubkey contact-pubkey-or-hash (:contacts/contacts db)) + (do + (log/warn "failed to lookup contact from hash, not logged in") + contact-pubkey-or-hash))) + contact-pubkey-or-hash)) + + (defn parse-notification-v1-payload [msg-json] + (let [msg (js/JSON.parse msg-json)] + {:from (object/get msg "from") + :to (object/get msg "to")})) + + (defn parse-notification-v2-payload [msg-v2-json] + (let [msg (js/JSON.parse msg-v2-json)] + {:from (object/get msg "from") + :to (object/get msg "to") + :id (object/get msg "id")})) + + (defn decode-notification-payload [message-js] + ;; message-js.-data is Notification.data(): + ;; https://github.com/invertase/react-native-firebase/blob/adcbeac3d11585dd63922ef178ff6fd886d5aa9b/src/modules/notifications/Notification.js#L79 + (let [data-js (.. message-js -data) + msg-v2-json (object/get data-js "msg-v2")] + (try + (let [payload (if msg-v2-json + (parse-notification-v2-payload msg-v2-json) + (parse-notification-v1-payload (object/get data-js "msg")))] + (if (valid-notification-payload? payload) + payload + (log/warn "failed to retrieve notification payload from" + (js/JSON.stringify data-js)))) + (catch :default e + (log/debug "failed to parse" (js/JSON.stringify data-js) + "exception:" e))))) + + (defn rehydrate-payload + [cofx {:keys [from to id] :as decoded-payload}] + "Takes a payload with hashed pubkeys and returns a payload with the real + (matched) pubkeys" + {:from (lookup-contact-pubkey-from-hash cofx from) + :to (lookup-contact-pubkey-from-hash cofx to) + ;; TODO: Rehydrate message id + :id id}) + + (defn- build-notification [{:keys [title body decoded-payload]}] + (let [native-notification + (clj->js + (merge + {:title title + :body body + :data (clj->js (encode-notification-payload decoded-payload)) + :sound sound-name} + (when-let [msg-id (:id decoded-payload)] + ;; We must prefix the notification ID, otherwise it will + ;; cause a crash in iOS + {:notificationId (str "hash:" msg-id)})))] + (firebase.notifications.Notification. + native-notification (.notifications firebase)))) + + (defn display-notification [{:keys [title body] :as params}] + (let [notification (build-notification params)] + (when platform/android? + (.. notification + (-android.setChannelId channel-id) + (-android.setAutoCancel true) + (-android.setPriority firebase.notifications.Android.Priority.High) + (-android.setCategory firebase.notifications.Android.Category.Message) + (-android.setGroup group-id) + (-android.setSmallIcon icon))) + (.. firebase + notifications + (displayNotification notification) + (then #(log/debug "Display Notification" title body)) + (catch (fn [error] + (log/debug "Display Notification error" title body error)))))) + + (defn get-fcm-token [] + (-> (.getToken (.messaging firebase)) + (.then (fn [x] + (log/debug "get-fcm-token:" x) + (re-frame/dispatch + [:notifications.callback/get-fcm-token-success x]))))) + (defn create-notification-channel [] - (let [channel (firebase.notifications.Android.Channel. channel-id - channel-name - firebase.notifications.Android.Importance.Max)] + (let [channel (firebase.notifications.Android.Channel. + channel-id + channel-name + firebase.notifications.Android.Importance.High)] (.setSound channel sound-name) (.setShowBadge channel true) (.enableVibration channel true) @@ -74,88 +196,163 @@ (then #(log/debug "Notification channel created:" channel-id) #(log/error "Notification channel creation error:" channel-id %))))) - (fx/defn handle-push-notification - [{:keys [db] :as cofx} {:keys [from to] :as event}] - (let [current-public-key (accounts.db/current-public-key cofx)] + (defn- show-notification? + "Ignore push notifications from unknown contacts or removed chats" + [{:keys [db] :as cofx} {:keys [from] :as rehydrated-payload}] + (and (valid-notification-payload? rehydrated-payload) + (accounts.db/logged-in? cofx) + (some #(= (:public-key %) from) + (vals (:contacts/contacts db))) + (some #(= (:chat-id %) from) + (vals (:chats db))))) + + (fx/defn handle-on-message + [{:keys [db] :as cofx} decoded-payload {:keys [force]}] + (let [view-id (:view-id db) + current-chat-id (:current-chat-id db) + app-state (:app-state db) + rehydrated-payload (rehydrate-payload cofx decoded-payload) + from (:from rehydrated-payload)] + (log/debug "handle-on-message" "app-state:" app-state + "view-id:" view-id "current-chat-id:" current-chat-id + "from:" from "force:" force) + (when (or force + (and + (not= app-state "active") + (show-notification? cofx rehydrated-payload))) + {:db + (assoc-in db [:push-notifications/stored (:to rehydrated-payload)] + (js/JSON.stringify (clj->js rehydrated-payload))) + :notifications/display-notification + {:title (i18n/label :notifications-new-message-title) + :body (i18n/label :notifications-new-message-body) + :decoded-payload rehydrated-payload}}))) + + (fx/defn handle-push-notification-open + [{:keys [db] :as cofx} decoded-payload {:keys [stored?] :as ctx}] + (let [current-public-key (accounts.db/current-public-key cofx) + nav-opts (when stored? {:navigation-reset? true}) + rehydrated-payload (rehydrate-payload cofx decoded-payload) + from (:from rehydrated-payload) + to (:to rehydrated-payload)] + (log/debug "handle-push-notification-open" + "current-public-key:" current-public-key + "rehydrated-payload:" rehydrated-payload "stored?:" stored?) (if (= to current-public-key) (fx/merge cofx {:db (update db :push-notifications/stored dissoc to)} - (chat-model/navigate-to-chat from nil)) - {:db (assoc-in db [:push-notifications/stored to] from)}))) + (chat-model/navigate-to-chat from nav-opts)) + {:db (assoc-in db [:push-notifications/stored to] + (js/JSON.stringify (clj->js rehydrated-payload)))}))) - (defn parse-notification-payload [s] - (try - (js/JSON.parse s) - (catch :default _ - #js {}))) + ;; https://github.com/invertase/react-native-firebase/blob/adcbeac3d11585dd63922ef178ff6fd886d5aa9b/src/modules/notifications/Notification.js#L13 + (defn handle-notification-open-event [event] + (log/debug "handle-notification-open-event" event) + (let [decoded-payload (decode-notification-payload (.. event -notification))] + (when decoded-payload + (re-frame/dispatch + [:notifications/notification-open-event-received decoded-payload nil])))) - (defn handle-notification-event [event] - (let [msg (object/get (.. event -notification -data) "msg") - data (parse-notification-payload msg) - from (object/get data "from") - to (object/get data "to")] - (log/debug "on notification" (pr-str msg)) - (when (and from to) - (re-frame/dispatch [:notifications/notification-event-received {:from from - :to to}])))) - - (defn handle-initial-push-notification - [] + (defn handle-initial-push-notification [] + "This method handles pending push notifications. + It is only needed to handle PNs from legacy clients + (which use firebase.notifications API)" + (log/debug "Handle initial push notifications") (.. firebase notifications getInitialNotification (then (fn [event] + (log/debug "getInitialNotification" event) (when event - (handle-notification-event event)))))) + (handle-notification-open-event event)))))) - (defn on-notification-opened [] + (defn setup-token-refresh-callback [] + (.onTokenRefresh + (.messaging firebase) + (fn [x] + (log/debug "onTokenRefresh:" x) + (re-frame/dispatch [:notifications.callback/get-fcm-token-success x])))) + + (defn setup-on-notification-callback [] + "Calling onNotification is only needed so that we're able to receive PNs" + "while in foreground from older clients who are still relying" + "on the notifications API. Once that is no longer a consideration" + "we can remove this method" + (log/debug "calling onNotification") + (.onNotification + (.notifications firebase) + (fn [message-js] + (log/debug "handle-on-notification-callback called") + (let [decoded-payload (decode-notification-payload message-js)] + (log/debug "handle-on-notification-callback payload:" decoded-payload) + (when decoded-payload + (re-frame/dispatch + [:notifications.callback/on-message decoded-payload])))))) + + (defn setup-on-message-callback [] + (log/debug "calling onMessage") + (.onMessage + (.messaging firebase) + (fn [message-js] + (log/debug "handle-on-message-callback called") + (let [decoded-payload (decode-notification-payload message-js)] + (log/debug "handle-on-message-callback decoded-payload:" + decoded-payload) + (when decoded-payload + (re-frame/dispatch + [:notifications.callback/on-message decoded-payload])))))) + + (defn setup-on-notification-opened-callback [] + (log/debug "setup-on-notification-opened-callback") (.. firebase notifications - (onNotificationOpened handle-notification-event))) + (onNotificationOpened handle-notification-open-event))) (defn init [] - (on-refresh-fcm-token) - (on-notification) - (on-notification-opened) + (log/debug "Init notifications") + (setup-token-refresh-callback) + (setup-on-message-callback) + (setup-on-notification-callback) + (setup-on-notification-opened-callback) (when platform/android? - (create-notification-channel))) + (create-notification-channel)) + (handle-initial-push-notification))) - (defn display-notification [{:keys [title body from to]}] - (let [notification (firebase.notifications.Notification.)] - (.. notification - (setTitle title) - (setBody body) - (setData (js/JSON.stringify #js {:from from - :to to})) - (setSound sound-name) - (-android.setChannelId channel-id) - (-android.setAutoCancel true) - (-android.setPriority firebase.notifications.Android.Priority.Max) - (-android.setGroup group-id) - (-android.setGroupSummary true) - (-android.setSmallIcon icon)) - (.. firebase - notifications - (displayNotification notification) - (then #(log/debug "Display Notification" title body)) - (then #(log/debug "Display Notification error" title body)))))) - -(fx/defn process-stored-event [cofx address] +(fx/defn process-stored-event [cofx address stored-pns] (when-not platform/desktop? - (let [to (get-in cofx [:db :accounts/accounts address :public-key]) - from (get-in cofx [:db :push-notifications/stored to])] - (when from - (handle-push-notification cofx - {:from from - :to to}))))) + (if (accounts.db/logged-in? cofx) + (let [current-account (get-in cofx [:db :account/account]) + current-address (:address current-account) + current-account-pubkey (:public-key current-account) + stored-pn-val-json (or (get stored-pns current-account-pubkey) + (get stored-pns (anonymize-pubkey current-account-pubkey))) + stored-pn-payload (if (= (first stored-pn-val-json) \{) + (js->clj (js/JSON.parse stored-pn-val-json) :keywordize-keys true) + {:from stored-pn-val-json + :to current-account-pubkey}) + from (lookup-contact-pubkey-from-hash cofx (:from stored-pn-payload)) + to (lookup-contact-pubkey-from-hash cofx (:to stored-pn-payload))] + (when (and from + (= address current-address)) + (log/debug "process-stored-event" "address" address "from" from "to" to) + (handle-push-notification-open cofx + stored-pn-payload + {:stored? true}))) + (log/error "process-stored-event called without user being logged in!")))) (re-frame/reg-fx :notifications/display-notification display-notification) (re-frame/reg-fx - :notifications/handle-initial-push-notification - handle-initial-push-notification) + :notifications/init + (fn [] + (cond + platform/android? + (init) + + platform/ios? + (utils/set-timeout init 100)))) (re-frame/reg-fx :notifications/get-fcm-token diff --git a/src/status_im/transport/message/core.cljs b/src/status_im/transport/message/core.cljs index 791b5e792e..89d3298566 100644 --- a/src/status_im/transport/message/core.cljs +++ b/src/status_im/transport/message/core.cljs @@ -87,7 +87,7 @@ (fx/merge cofx (remove-hash envelope-hash) (check-confirmations status chat-id message-id) - (models.message/send-push-notification fcm-token status))))))) + (models.message/send-push-notification message-id fcm-token status))))))) (fx/defn set-contact-message-envelope-hash [{:keys [db] :as cofx} chat-id envelope-hash] diff --git a/src/status_im/utils/config.cljs b/src/status_im/utils/config.cljs index bd1ae352ee..8cdbe96ae5 100644 --- a/src/status_im/utils/config.cljs +++ b/src/status_im/utils/config.cljs @@ -24,7 +24,6 @@ (def mailserver-confirmations-enabled? (enabled? (get-config :MAILSERVER_CONFIRMATIONS_ENABLED))) (def mainnet-warning-enabled? (enabled? (get-config :MAINNET_WARNING_ENABLED 0))) (def pfs-encryption-enabled? (enabled? (get-config :PFS_ENCRYPTION_ENABLED "0"))) -(def in-app-notifications-enabled? (enabled? (get-config :IN_APP_NOTIFICATIONS_ENABLED 0))) (def cached-webviews-enabled? (enabled? (get-config :CACHED_WEBVIEWS_ENABLED 0))) (def rn-bridge-threshold-warnings-enabled? (enabled? (get-config :RN_BRIDGE_THRESHOLD_WARNINGS 0))) (def extensions-enabled? (enabled? (get-config :EXTENSIONS 0))) diff --git a/src/status_im/utils/universal_links/core.cljs b/src/status_im/utils/universal_links/core.cljs index 9fe8276da1..cb1d29448a 100644 --- a/src/status_im/utils/universal_links/core.cljs +++ b/src/status_im/utils/universal_links/core.cljs @@ -55,11 +55,11 @@ (re-frame/dispatch [:handle-universal-link url]))) (fx/defn handle-browse [cofx url] - (log/info "universal-links: handling browse " url) + (log/info "universal-links: handling browse" url) {:browser/show-browser-selection url}) (fx/defn handle-public-chat [cofx public-chat] - (log/info "universal-links: handling public chat " public-chat) + (log/info "universal-links: handling public chat" public-chat) (chat/start-public-chat cofx public-chat {:navigation-reset? true})) (fx/defn handle-view-profile [{:keys [db] :as cofx} public-key] diff --git a/test/cljs/status_im/test/notifications/core.cljs b/test/cljs/status_im/test/notifications/core.cljs index 0e5f6563f0..2fe580acb7 100644 --- a/test/cljs/status_im/test/notifications/core.cljs +++ b/test/cljs/status_im/test/notifications/core.cljs @@ -2,34 +2,43 @@ (:require [cljs.test :refer-macros [deftest is testing]] [status-im.notifications.core :as notifications])) -(deftest test-handle-push-notification +(deftest test-handle-push-notification-open + (testing "user's signing in having opened PN while signed out" + (is (= {:db {:account/account {:public-key "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"} + :push-notifications/stored {}} + :dispatch [:navigate-to-chat "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de" {:navigation-reset? true}]} + (notifications/handle-push-notification-open {:db {:push-notifications/stored {} + :account/account {:public-key "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}}} + [:push-notification-opened {:from "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de" + :to "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2" + :stored? true}])))) (testing "user's signed in" (is (= {:db {:account/account {:public-key "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"} :push-notifications/stored {}} :dispatch [:navigate-to-chat "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de"]} - (notifications/handle-push-notification {:db {:push-notifications/stored {} - :account/account {:publi-key "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}}} - [:push-notification-opened {:from "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de" - :to "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}])))) + (notifications/handle-push-notification-open {:db {:push-notifications/stored {} + :account/account {:public-key "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}}} + [:push-notification-opened {:from "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de" + :to "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}])))) (testing "user's signed in into another account" (is (= {} - (notifications/handle-push-notification {:db {:account/account {:public-key "0x04bc8bf4a91ab726bd98f2c54b3036caacaeea527867945ab839e9ad4e62696856d7f7fa485f68304de357e38a1553eac5592706a16fcf71fd821bbd6c796f9ab3"}}} - [:push-notification-opened {:from "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de" - :to "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}])))) + (notifications/handle-push-notification-open {:db {:account/account {:public-key "0x04bc8bf4a91ab726bd98f2c54b3036caacaeea527867945ab839e9ad4e62696856d7f7fa485f68304de357e38a1553eac5592706a16fcf71fd821bbd6c796f9ab3"}}} + [:push-notification-opened {:from "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de" + :to "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}])))) (testing "user's not signed in" (is (= {:db {:accounts/accounts {"bd36cd64e2621b054a3b7464ff1b3c4c304880e7" {:address "bd36cd64e2621b054a3b7464ff1b3c4c304880e7" :photo-path "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAMAAAC7IEhfAAADAFBMVEX" :name "Bob" :public-key "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}} - :account/account {:public-key nil} + :account/account {:public-key nil} :push-notifications/stored {"0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2" "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de"}} :dispatch [:ui/open-login "bd36cd64e2621b054a3b7464ff1b3c4c304880e7" "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAMAAAC7IEhfAAADAFBMVEX" "Bob"]} - (notifications/handle-push-notification {:db {:accounts/accounts {"bd36cd64e2621b054a3b7464ff1b3c4c304880e7" {:address "bd36cd64e2621b054a3b7464ff1b3c4c304880e7" - :photo-path "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAMAAAC7IEhfAAADAFBMVEX" - :name "Bob" - :public-key "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}} - :account/account {:public-key nil} - :push-notifications/stored {}}} - [:push-notification-opened {:from "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de" - :to "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}]))))) + (notifications/handle-push-notification-open {:db {:accounts/accounts {"bd36cd64e2621b054a3b7464ff1b3c4c304880e7" {:address "bd36cd64e2621b054a3b7464ff1b3c4c304880e7" + :photo-path "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAMAAAC7IEhfAAADAFBMVEX" + :name "Bob" + :public-key "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}} + :account/account {:public-key nil} + :push-notifications/stored {}}} + [:push-notification-opened {:from "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de" + :to "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}])))))