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
This commit is contained in:
Pedro Pombeiro 2018-11-20 19:36:11 +01:00 committed by Roman Volosovskyi
parent 4af2073388
commit 5a69b4198e
No known key found for this signature in database
GPG Key ID: 0238A4B5ECEE70DE
26 changed files with 462 additions and 215 deletions

1
.gitignore vendored
View File

@ -156,3 +156,4 @@ conan*.txt
conanbuildinfo.* conanbuildinfo.*
conan.cmake conan.cmake
/yarn-error.log /yarn-error.log
/default.realm/

View File

@ -211,6 +211,8 @@ android-ports: ##@other Add proxies to Android Device/Simulator
adb reverse tcp:4567 tcp:4567 adb reverse tcp:4567 tcp:4567
adb forward tcp:5561 tcp:5561 adb forward tcp:5561 tcp:5561
android-logcat:
adb logcat | grep -e StatusModule -e ReactNativeJS -e StatusNativeLogs
startdev-%: startdev-%:
$(eval SYSTEM := $(word 2, $(subst -, , $@))) $(eval SYSTEM := $(word 2, $(subst -, , $@)))

View File

@ -1 +1 @@
181221-204011-e80de6 0.19.0-beta.1

View File

@ -68,3 +68,7 @@
-dontwarn java.nio.file.* -dontwarn java.nio.file.*
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn okio.** -dontwarn okio.**
# Firebase
-keep class io.invertase.firebase.** { *; }
-dontwarn io.invertase.firebase.**

View File

@ -13,7 +13,7 @@ pipeline {
timestamps() timestamps()
disableConcurrentBuilds() disableConcurrentBuilds()
/* Prevent Jenkins jobs from running forever */ /* Prevent Jenkins jobs from running forever */
timeout(time: 35, unit: 'MINUTES') timeout(time: 45, unit: 'MINUTES')
/* Limit builds retained */ /* Limit builds retained */
buildDiscarder(logRotator( buildDiscarder(logRotator(
numToKeepStr: '10', numToKeepStr: '10',

View File

@ -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* *There is no rigid structure on how to organize code inside modules outside of core and db namespaces*
``` ```txt
- events.cljs - events.cljs
- subs.cljs - subs.cljs
- notifications - notifications
@ -63,11 +63,12 @@ These guidelines make db.cljs namespaces the place to go when making changes to
- events must only contain a function call defined in a module - events must only contain a function call defined in a module
```clojure ```clojure
(handlers/register-handler-fx (handlers/register-handler-fx
:notifications/handle-push-notification :notifications/handle-push-notification-open
(fn [cofx [_ event]] (fn [cofx [_ event]]
(notifications/handle-push-notification event cofx))) (notifications/handle-push-notification-open event cofx)))
``` ```
- events must use synthetic namespaces: - events must use synthetic namespaces:
- `:module.ui/` for user triggered events - `: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.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/` for internal events, examples are time based events marked `-timed-out`, external changes marked `-changed` or reception of external events marked `-received`.

View File

@ -105,12 +105,12 @@ QList<ModuleMethod *> DesktopNotification::methodsToExport() {
QVariantMap DesktopNotification::constantsToExport() { return QVariantMap(); } 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); Q_D(DesktopNotification);
qCDebug(NOTIFICATION) << "::sendNotification"; qCDebug(NOTIFICATION) << "::displayNotification";
if (m_appHasFocus) { 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; return;
} }

View File

@ -35,7 +35,7 @@ public:
QList<ModuleMethod*> methodsToExport() override; QList<ModuleMethod*> methodsToExport() override;
QVariantMap constantsToExport() 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); Q_INVOKABLE void setDockBadgeLabel(const QString label);
private: private:
QScopedPointer<DesktopNotificationPrivate> d_ptr; QScopedPointer<DesktopNotificationPrivate> d_ptr;

View File

@ -81,7 +81,7 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
} }
@Override @Override
public void onHostResume() { // Actvity `onResume` public void onHostResume() { // Activity `onResume`
module = this; module = this;
Activity currentActivity = getCurrentActivity(); Activity currentActivity = getCurrentActivity();
if (currentActivity == null) { if (currentActivity == null) {
@ -459,7 +459,7 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
} }
@ReactMethod @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"); Log.d(TAG, "notifyUsers");
if (!checkAvailability()) { if (!checkAvailability()) {
callback.invoke(false); callback.invoke(false);
@ -469,7 +469,7 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
Runnable r = new Runnable() { Runnable r = new Runnable() {
@Override @Override
public void run() { public void run() {
String res = Statusgo.NotifyUsers(message, payloadJSON, tokensJSON); String res = Statusgo.NotifyUsers(dataPayloadJSON, tokensJSON);
callback.invoke(res); callback.invoke(res);
} }

View File

@ -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); Q_D(RCTStatus);
qCDebug(RCTSTATUS) << "::notifyUsers call - callbackId:" << callbackId; qCDebug(RCTSTATUS) << "::notifyUsers call - callbackId:" << callbackId;
QtConcurrent::run([&](QString token, QString payloadJSON, QString tokensJSON, double callbackId) { QtConcurrent::run([&](QString dataPayloadJSON, QString tokensJSON, double callbackId) {
const char* result = NotifyUsers(token.toUtf8().data(), payloadJSON.toUtf8().data(), tokensJSON.toUtf8().data()); const char* result = NotifyUsers(dataPayloadJSON.toUtf8().data(), tokensJSON.toUtf8().data());
logStatusGoResult("::notifyUsers Notify", result); logStatusGoResult("::notifyUsers Notify", result);
d->bridge->invokePromiseCallback(callbackId, QVariantList{result}); d->bridge->invokePromiseCallback(callbackId, QVariantList{result});
}, token, payloadJSON, tokensJSON, callbackId); }, dataPayloadJSON, tokensJSON, callbackId);
} }

View File

@ -38,7 +38,7 @@ public:
Q_INVOKABLE void startNode(QString configString); Q_INVOKABLE void startNode(QString configString);
Q_INVOKABLE void stopNode(); Q_INVOKABLE void stopNode();
Q_INVOKABLE void createAccount(QString password, double callbackId); 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 sendLogs(QString dbJSON);
Q_INVOKABLE void addPeer(QString enode, double callbackId); Q_INVOKABLE void addPeer(QString enode, double callbackId);
Q_INVOKABLE void recoverAccount(QString passphrase, QString password, double callbackId); Q_INVOKABLE void recoverAccount(QString passphrase, QString password, double callbackId);

View File

@ -185,11 +185,10 @@ RCT_EXPORT_METHOD(createAccount:(NSString *)password
//////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////
#pragma mark - NotifyUsers method #pragma mark - NotifyUsers method
//////////////////////////////////////////////////////////////////// notifyUsers //////////////////////////////////////////////////////////////////// notifyUsers
RCT_EXPORT_METHOD(notifyUsers:(NSString *)message RCT_EXPORT_METHOD(notifyUsers:(NSString *)dataPayloadJSON
payloadJSON:(NSString *)payloadJSON
tokensJSON:(NSString *)tokensJSON tokensJSON:(NSString *)tokensJSON
callback:(RCTResponseSenderBlock)callback) { 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]]); callback(@[[NSString stringWithUTF8String: result]]);
#if DEBUG #if DEBUG
NSLog(@"NotifyUsers() method called"); NSLog(@"NotifyUsers() method called");

View File

@ -40,9 +40,7 @@
(.addEventListener react/app-state "change" app-state-change-handler)) (.addEventListener react/app-state "change" app-state-change-handler))
:component-did-mount :component-did-mount
(fn [this] (fn [this]
(dispatch [:set-initial-props (reagent/props this)]) (dispatch [:set-initial-props (reagent/props this)]))
;; TODO(oskarth): Background click_action handler
(notifications/init))
:component-will-unmount :component-will-unmount
(fn [] (fn []
(.stop react/http-bridge) (.stop react/http-bridge)

View File

@ -121,7 +121,7 @@
(get-in db [:account/account :desktop-notifications?]) (get-in db [:account/account :desktop-notifications?])
(< (time/seconds-ago (time/to-date timestamp)) constants/one-earth-day)) (< (time/seconds-ago (time/to-date timestamp)) constants/one-earth-day))
(let [{:keys [title body prioritary?]} (build-desktop-notification cofx message)] (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 (fx/merge cofx
{:db (cond-> {:db (cond->
(-> db (-> db
@ -159,19 +159,6 @@
message message
(assoc message :clock-value (utils.clocks/send last-clock-value)))) (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 (defn check-response-to
[{{:keys [response-to response-to-v2]} :content :as message} [{{:keys [response-to response-to-v2]} :content :as message}
old-id->message] old-id->message]
@ -210,7 +197,6 @@
current-chat? :seen current-chat? :seen
:else :received)) :else :received))
(commands-receiving/receive message) (commands-receiving/receive message)
(display-notification chat-id)
(send-message-seen chat-id message-id (and (not group-chat) (send-message-seen chat-id message-id (and (not group-chat)
current-chat? current-chat?
(not (= constants/system from)) (not (= constants/system from))
@ -391,14 +377,14 @@
(add-own-status chat-id message-id :sending) (add-own-status chat-id message-id :sending)
(send chat-id message-id wrapped-record)))) (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)) (when (and fcm-token (= status :sent))
{:send-notification {:message (js/JSON.stringify #js {:from (accounts.db/current-public-key cofx) (let [payload {:from (accounts.db/current-public-key cofx)
:to (get-in cofx [:db :current-chat-id])}) :to (get-in cofx [:db :current-chat-id])
:payload {:title (i18n/label :notifications-new-message-title) :id message-id}]
:body (i18n/label :notifications-new-message-body) {:send-notification {:data-payload (notifications/encode-notification-payload payload)
:sound notifications/sound-name} :tokens [fcm-token]}})))
:tokens [fcm-token]}}))
(fx/defn update-message-status [{:keys [db]} chat-id message-id status] (fx/defn update-message-status [{:keys [db]} chat-id message-id status]
(let [from (get-in db [:chats chat-id :messages message-id :from]) (let [from (get-in db [:chats chat-id :messages message-id :from])
@ -484,8 +470,13 @@
(re-frame/reg-fx (re-frame/reg-fx
:send-notification :send-notification
(fn [{:keys [message payload tokens]}] (fn [{:keys [data-payload tokens]}]
(let [payload-json (types/clj->json payload) "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)] tokens-json (types/clj->json tokens)]
(log/debug "send-notification message: " message " payload-json: " payload-json " tokens-json: " tokens-json) (log/debug "send-notification data-payload-json:" data-payload-json "tokens-json:" tokens-json)
(status/notify-users {:message message :payload payload-json :tokens tokens-json} #(log/debug "send-notification cb result: " %))))) ;; 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: " %)))))

View File

@ -1,7 +1,9 @@
(ns status-im.core (ns status-im.core
(:require [re-frame.core :as re-frame] (:require [re-frame.core :as re-frame]
[status-im.utils.error-handler :as error-handler] [status-im.utils.error-handler :as error-handler]
[status-im.utils.platform :as platform]
[status-im.ui.components.react :as react] [status-im.ui.components.react :as react]
[status-im.notifications.background :as background-messaging]
[reagent.core :as reagent] [reagent.core :as reagent]
status-im.transport.impl.receive status-im.transport.impl.receive
status-im.transport.impl.send status-im.transport.impl.send
@ -18,4 +20,6 @@
(log/set-level! config/log-level) (log/set-level! config/log-level)
(error-handler/register-exception-handler!) (error-handler/register-exception-handler!)
(re-frame/dispatch [:init/app-started]) (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)))

View File

@ -830,9 +830,9 @@
;; notifications module ;; notifications module
(handlers/register-handler-fx (handlers/register-handler-fx
:notifications/notification-event-received :notifications/notification-open-event-received
(fn [cofx [_ event]] (fn [cofx [_ decoded-payload ctx]]
(notifications/handle-push-notification cofx event))) (notifications/handle-push-notification-open cofx decoded-payload ctx)))
(handlers/register-handler-fx (handlers/register-handler-fx
:notifications.callback/get-fcm-token-success :notifications.callback/get-fcm-token-success
@ -849,6 +849,11 @@
(fn [cofx _] (fn [cofx _]
(accounts/show-mainnet-is-default-alert 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 ;; hardwallet module
(handlers/register-handler-fx (handlers/register-handler-fx

View File

@ -19,7 +19,8 @@
(then #(re-frame/dispatch [:hardwallet.callback/check-nfc-support-success %]))))) (then #(re-frame/dispatch [:hardwallet.callback/check-nfc-support-success %])))))
(defn check-nfc-enabled [] (defn check-nfc-enabled []
(when platform/android? (when (and platform/android?
config/hardwallet-enabled?)
(.. keycard (.. keycard
nfcIsEnabled nfcIsEnabled
(then #(re-frame/dispatch [:hardwallet.callback/check-nfc-enabled-success %]))))) (then #(re-frame/dispatch [:hardwallet.callback/check-nfc-enabled-success %])))))

View File

@ -77,7 +77,7 @@
{:init/get-device-UUID nil {:init/get-device-UUID nil
:init/restore-native-settings nil :init/restore-native-settings nil
:ui/listen-to-window-dimensions-change nil :ui/listen-to-window-dimensions-change nil
:notifications/handle-initial-push-notification nil :notifications/init nil
:network/listen-to-network-status nil :network/listen-to-network-status nil
:network/listen-to-connection-status nil :network/listen-to-connection-status nil
:hardwallet/check-nfc-support nil :hardwallet/check-nfc-support nil
@ -90,7 +90,8 @@
"Initialize db to initial state" "Initialize db to initial state"
[{{:keys [status-module-initialized? view-id hardwallet [{{:keys [status-module-initialized? view-id hardwallet
initial-props desktop/desktop 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] :node/keys [status]
:or {network (get app-db :network)}} :db}] :or {network (get app-db :network)}} :db}]
{:db (assoc app-db {:db (assoc app-db
@ -105,7 +106,8 @@
:network network :network network
:hardwallet hardwallet :hardwallet hardwallet
:device-UUID device-UUID :device-UUID device-UUID
:view-id view-id)}) :view-id view-id
:push-notifications/stored stored)})
(fx/defn initialize-app (fx/defn initialize-app
[cofx encryption-key] [cofx encryption-key]
@ -140,8 +142,13 @@
(let [{{:accounts/keys [accounts] :as db} :db} cofx] (let [{{:accounts/keys [accounts] :as db} :db} cofx]
(if (empty? accounts) (if (empty? accounts)
(navigation/navigate-to-clean cofx :intro nil) (navigation/navigate-to-clean cofx :intro nil)
(let [account-with-notification (first (keys (:push-notifications/stored db))) (let [account-with-notification
selection-fn (if (not-empty 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] #(filter (fn [account]
(= account-with-notification (= account-with-notification
(:public-key account))) (:public-key account)))
@ -193,12 +200,12 @@
(= view-id :create-account) (= view-id :create-account)
(assoc-in [:accounts/create :step] :enter-name))})) (assoc-in [:accounts/create :step] :enter-name))}))
(defn login-only-events [cofx address] (defn login-only-events [cofx address stored-pns]
(fx/merge cofx (fx/merge cofx
{:notifications/request-notifications-permissions nil} {:notifications/request-notifications-permissions nil}
(navigation/navigate-to-cofx :home nil) (navigation/navigate-to-cofx :home nil)
(universal-links/process-stored-event) (universal-links/process-stored-event)
(notifications/process-stored-event address) (notifications/process-stored-event address stored-pns)
(when platform/desktop? (when platform/desktop?
(chat-model/update-dock-badge-label)))) (chat-model/update-dock-badge-label))))
@ -213,7 +220,8 @@
(= (get-in cofx [:db :view-id]) (= (get-in cofx [:db :view-id])
:hardwallet-success)) :hardwallet-success))
(fx/defn initialize-account [cofx address] (fx/defn initialize-account [{:keys [db] :as cofx} address]
(let [stored-pns (:push-notifications/stored db)]
(fx/merge cofx (fx/merge cofx
{:notifications/get-fcm-token nil} {:notifications/get-fcm-token nil}
(initialize-account-db address) (initialize-account-db address)
@ -228,7 +236,7 @@
(accounts.update/update-sign-in-time) (accounts.update/update-sign-in-time)
#(when-not (or (creating-account? %) #(when-not (or (creating-account? %)
(finishing-hardwallet-setup? %)) (finishing-hardwallet-setup? %))
(login-only-events % address)))) (login-only-events % address stored-pns)))))
(re-frame/reg-fx (re-frame/reg-fx
:init/init-store :init/init-store

View File

@ -35,8 +35,7 @@
(.addEventListener react/app-state "change" app-state-change-handler)) (.addEventListener react/app-state "change" app-state-change-handler))
:component-did-mount :component-did-mount
(fn [this] (fn [this]
(dispatch [:set-initial-props (reagent/props this)]) (dispatch [:set-initial-props (reagent/props this)]))
(notifications/init))
:component-will-unmount :component-will-unmount
(fn [] (fn []
(.stop react/http-bridge) (.stop react/http-bridge)

View File

@ -84,9 +84,9 @@
true) true)
false)))))) 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 (when status
(call-module #(.notifyUsers status message payload tokens on-result)))) (call-module #(.notifyUsers status data-payload tokens on-result))))
(defn send-logs [dbJson] (defn send-logs [dbJson]
(when status (when status

View File

@ -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)))))))

View File

@ -2,15 +2,22 @@
(:require [goog.object :as object] (:require [goog.object :as object]
[re-frame.core :as re-frame] [re-frame.core :as re-frame]
[status-im.react-native.js-dependencies :as rn] [status-im.react-native.js-dependencies :as rn]
[status-im.js-dependencies :as dependencies]
[taoensso.timbre :as log] [taoensso.timbre :as log]
[status-im.i18n :as i18n]
[status-im.accounts.db :as accounts.db] [status-im.accounts.db :as accounts.db]
[status-im.chat.models :as chat-model] [status-im.chat.models :as chat-model]
[status-im.utils.platform :as platform] [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 ;; Work in progress namespace responsible for push notifications and interacting
;; with Firebase Cloud Messaging. ;; 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? (when-not platform/desktop?
(def firebase (object/get rn/react-native-firebase "default"))) (def firebase (object/get rn/react-native-firebase "default")))
@ -28,42 +35,157 @@
(log/debug "notifications-denied") (log/debug "notifications-denied")
(re-frame/dispatch [:notifications.callback/request-notifications-permissions-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? (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-id "status-im")
(def channel-name "Status") (def channel-name "Status")
(def sound-name "message.wav") (def sound-name "message.wav")
(def group-id "im.status.ethereum.MESSAGE") (def group-id "im.status.ethereum.MESSAGE")
(def icon "ic_stat_status_notification") (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 [] (defn create-notification-channel []
(let [channel (firebase.notifications.Android.Channel. channel-id (let [channel (firebase.notifications.Android.Channel.
channel-id
channel-name channel-name
firebase.notifications.Android.Importance.Max)] firebase.notifications.Android.Importance.High)]
(.setSound channel sound-name) (.setSound channel sound-name)
(.setShowBadge channel true) (.setShowBadge channel true)
(.enableVibration channel true) (.enableVibration channel true)
@ -74,88 +196,163 @@
(then #(log/debug "Notification channel created:" channel-id) (then #(log/debug "Notification channel created:" channel-id)
#(log/error "Notification channel creation error:" channel-id %))))) #(log/error "Notification channel creation error:" channel-id %)))))
(fx/defn handle-push-notification (defn- show-notification?
[{:keys [db] :as cofx} {:keys [from to] :as event}] "Ignore push notifications from unknown contacts or removed chats"
(let [current-public-key (accounts.db/current-public-key cofx)] [{: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) (if (= to current-public-key)
(fx/merge cofx (fx/merge cofx
{:db (update db :push-notifications/stored dissoc to)} {:db (update db :push-notifications/stored dissoc to)}
(chat-model/navigate-to-chat from nil)) (chat-model/navigate-to-chat from nav-opts))
{:db (assoc-in db [:push-notifications/stored to] from)}))) {:db (assoc-in db [:push-notifications/stored to]
(js/JSON.stringify (clj->js rehydrated-payload)))})))
(defn parse-notification-payload [s] ;; https://github.com/invertase/react-native-firebase/blob/adcbeac3d11585dd63922ef178ff6fd886d5aa9b/src/modules/notifications/Notification.js#L13
(try (defn handle-notification-open-event [event]
(js/JSON.parse s) (log/debug "handle-notification-open-event" event)
(catch :default _ (let [decoded-payload (decode-notification-payload (.. event -notification))]
#js {}))) (when decoded-payload
(re-frame/dispatch
[:notifications/notification-open-event-received decoded-payload nil]))))
(defn handle-notification-event [event] (defn handle-initial-push-notification []
(let [msg (object/get (.. event -notification -data) "msg") "This method handles pending push notifications.
data (parse-notification-payload msg) It is only needed to handle PNs from legacy clients
from (object/get data "from") (which use firebase.notifications API)"
to (object/get data "to")] (log/debug "Handle initial push notifications")
(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
[]
(.. firebase (.. firebase
notifications notifications
getInitialNotification getInitialNotification
(then (fn [event] (then (fn [event]
(log/debug "getInitialNotification" event)
(when 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 (.. firebase
notifications notifications
(onNotificationOpened handle-notification-event))) (onNotificationOpened handle-notification-open-event)))
(defn init [] (defn init []
(on-refresh-fcm-token) (log/debug "Init notifications")
(on-notification) (setup-token-refresh-callback)
(on-notification-opened) (setup-on-message-callback)
(setup-on-notification-callback)
(setup-on-notification-opened-callback)
(when platform/android? (when platform/android?
(create-notification-channel))) (create-notification-channel))
(handle-initial-push-notification)))
(defn display-notification [{:keys [title body from to]}] (fx/defn process-stored-event [cofx address stored-pns]
(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]
(when-not platform/desktop? (when-not platform/desktop?
(let [to (get-in cofx [:db :accounts/accounts address :public-key]) (if (accounts.db/logged-in? cofx)
from (get-in cofx [:db :push-notifications/stored to])] (let [current-account (get-in cofx [:db :account/account])
(when from current-address (:address current-account)
(handle-push-notification cofx current-account-pubkey (:public-key current-account)
{:from from stored-pn-val-json (or (get stored-pns current-account-pubkey)
:to to}))))) (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 (re-frame/reg-fx
:notifications/display-notification :notifications/display-notification
display-notification) display-notification)
(re-frame/reg-fx (re-frame/reg-fx
:notifications/handle-initial-push-notification :notifications/init
handle-initial-push-notification) (fn []
(cond
platform/android?
(init)
platform/ios?
(utils/set-timeout init 100))))
(re-frame/reg-fx (re-frame/reg-fx
:notifications/get-fcm-token :notifications/get-fcm-token

View File

@ -87,7 +87,7 @@
(fx/merge cofx (fx/merge cofx
(remove-hash envelope-hash) (remove-hash envelope-hash)
(check-confirmations status chat-id message-id) (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 (fx/defn set-contact-message-envelope-hash
[{:keys [db] :as cofx} chat-id envelope-hash] [{:keys [db] :as cofx} chat-id envelope-hash]

View File

@ -24,7 +24,6 @@
(def mailserver-confirmations-enabled? (enabled? (get-config :MAILSERVER_CONFIRMATIONS_ENABLED))) (def mailserver-confirmations-enabled? (enabled? (get-config :MAILSERVER_CONFIRMATIONS_ENABLED)))
(def mainnet-warning-enabled? (enabled? (get-config :MAINNET_WARNING_ENABLED 0))) (def mainnet-warning-enabled? (enabled? (get-config :MAINNET_WARNING_ENABLED 0)))
(def pfs-encryption-enabled? (enabled? (get-config :PFS_ENCRYPTION_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 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 rn-bridge-threshold-warnings-enabled? (enabled? (get-config :RN_BRIDGE_THRESHOLD_WARNINGS 0)))
(def extensions-enabled? (enabled? (get-config :EXTENSIONS 0))) (def extensions-enabled? (enabled? (get-config :EXTENSIONS 0)))

View File

@ -2,18 +2,27 @@
(:require [cljs.test :refer-macros [deftest is testing]] (:require [cljs.test :refer-macros [deftest is testing]]
[status-im.notifications.core :as notifications])) [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" (testing "user's signed in"
(is (= {:db {:account/account {:public-key "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"} (is (= {:db {:account/account {:public-key "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}
:push-notifications/stored {}} :push-notifications/stored {}}
:dispatch [:navigate-to-chat "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de"]} :dispatch [:navigate-to-chat "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de"]}
(notifications/handle-push-notification {:db {:push-notifications/stored {} (notifications/handle-push-notification-open {:db {:push-notifications/stored {}
:account/account {:publi-key "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}}} :account/account {:public-key "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}}}
[:push-notification-opened {:from "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de" [:push-notification-opened {:from "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de"
:to "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}])))) :to "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}]))))
(testing "user's signed in into another account" (testing "user's signed in into another account"
(is (= {} (is (= {}
(notifications/handle-push-notification {:db {:account/account {:public-key "0x04bc8bf4a91ab726bd98f2c54b3036caacaeea527867945ab839e9ad4e62696856d7f7fa485f68304de357e38a1553eac5592706a16fcf71fd821bbd6c796f9ab3"}}} (notifications/handle-push-notification-open {:db {:account/account {:public-key "0x04bc8bf4a91ab726bd98f2c54b3036caacaeea527867945ab839e9ad4e62696856d7f7fa485f68304de357e38a1553eac5592706a16fcf71fd821bbd6c796f9ab3"}}}
[:push-notification-opened {:from "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de" [:push-notification-opened {:from "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de"
:to "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}])))) :to "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}]))))
(testing "user's not signed in" (testing "user's not signed in"
@ -25,7 +34,7 @@
:push-notifications/stored {"0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2" :push-notifications/stored {"0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"
"0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de"}} "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de"}}
:dispatch [:ui/open-login "bd36cd64e2621b054a3b7464ff1b3c4c304880e7" "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAMAAAC7IEhfAAADAFBMVEX" "Bob"]} :dispatch [:ui/open-login "bd36cd64e2621b054a3b7464ff1b3c4c304880e7" "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAMAAAC7IEhfAAADAFBMVEX" "Bob"]}
(notifications/handle-push-notification {:db {:accounts/accounts {"bd36cd64e2621b054a3b7464ff1b3c4c304880e7" {:address "bd36cd64e2621b054a3b7464ff1b3c4c304880e7" (notifications/handle-push-notification-open {:db {:accounts/accounts {"bd36cd64e2621b054a3b7464ff1b3c4c304880e7" {:address "bd36cd64e2621b054a3b7464ff1b3c4c304880e7"
:photo-path "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAMAAAC7IEhfAAADAFBMVEX" :photo-path "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAMAAAC7IEhfAAADAFBMVEX"
:name "Bob" :name "Bob"
:public-key "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}} :public-key "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}}