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:
parent
4af2073388
commit
5a69b4198e
|
@ -156,3 +156,4 @@ conan*.txt
|
||||||
conanbuildinfo.*
|
conanbuildinfo.*
|
||||||
conan.cmake
|
conan.cmake
|
||||||
/yarn-error.log
|
/yarn-error.log
|
||||||
|
/default.realm/
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -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 -, , $@)))
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
181221-204011-e80de6
|
0.19.0-beta.1
|
||||||
|
|
|
@ -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.**
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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
|
||||||
|
@ -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
|
- fx producing functions called by events and other modules
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(def get-current-account
|
(def get-current-account
|
||||||
module.db/get-current-account)
|
module.db/get-current-account)
|
||||||
|
|
||||||
(defn set-current-account [{db :db :as cofx}]
|
(defn set-current-account [{db :db :as cofx}]
|
||||||
{:db (module.db/set-current-account db)})
|
{:db (module.db/set-current-account db)})
|
||||||
```
|
```
|
||||||
|
|
||||||
## db.cljs
|
## 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 always be declared with `register-handler-fx`, no `register-handler-db`
|
||||||
- events must never use the `trim-v` interceptor
|
- events must never use the `trim-v` interceptor
|
||||||
- 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.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.ui/` for user triggered events
|
||||||
- `:module/` for internal events, examples are time based events marked `-timed-out`, external changes marked `-changed` or reception of external events marked `-received`.
|
- `: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`.
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"
|
||||||
tokens-json (types/clj->json tokens)]
|
(let [data-payload-json (types/clj->json data-payload)
|
||||||
(log/debug "send-notification message: " message " payload-json: " payload-json " tokens-json: " tokens-json)
|
tokens-json (types/clj->json tokens)]
|
||||||
(status/notify-users {:message message :payload payload-json :tokens tokens-json} #(log/debug "send-notification cb result: " %)))))
|
(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: " %)))))
|
||||||
|
|
|
@ -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)))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 %])))))
|
||||||
|
|
|
@ -74,23 +74,24 @@
|
||||||
|
|
||||||
(fx/defn start-app [cofx]
|
(fx/defn start-app [cofx]
|
||||||
(fx/merge cofx
|
(fx/merge cofx
|
||||||
{: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
|
||||||
:hardwallet/check-nfc-enabled nil
|
:hardwallet/check-nfc-enabled nil
|
||||||
:hardwallet/start-module nil
|
:hardwallet/start-module nil
|
||||||
:hardwallet/register-card-events nil}
|
:hardwallet/register-card-events nil}
|
||||||
(initialize-keychain)))
|
(initialize-keychain)))
|
||||||
|
|
||||||
(fx/defn initialize-app-db
|
(fx/defn initialize-app-db
|
||||||
"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,13 +142,18 @@
|
||||||
(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?
|
||||||
#(filter (fn [account]
|
(notifications/lookup-contact-pubkey-from-hash
|
||||||
(= account-with-notification
|
cofx
|
||||||
(:public-key account)))
|
(first (keys (:push-notifications/stored db)))))
|
||||||
%)
|
selection-fn
|
||||||
#(sort-by :last-sign-in > %))
|
(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)))]
|
{:keys [address photo-path name]} (first (selection-fn (vals accounts)))]
|
||||||
(accounts.login/open-login cofx address photo-path name)))))
|
(accounts.login/open-login cofx address photo-path name)))))
|
||||||
|
|
||||||
|
@ -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,22 +220,23 @@
|
||||||
(= (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]
|
||||||
(fx/merge cofx
|
(let [stored-pns (:push-notifications/stored db)]
|
||||||
{:notifications/get-fcm-token nil}
|
(fx/merge cofx
|
||||||
(initialize-account-db address)
|
{:notifications/get-fcm-token nil}
|
||||||
(contact/load-contacts)
|
(initialize-account-db address)
|
||||||
(pairing/load-installations)
|
(contact/load-contacts)
|
||||||
#(when (dev-mode? %)
|
(pairing/load-installations)
|
||||||
(models.dev-server/start))
|
#(when (dev-mode? %)
|
||||||
(browser/initialize-browsers)
|
(models.dev-server/start))
|
||||||
|
(browser/initialize-browsers)
|
||||||
|
|
||||||
(browser/initialize-dapp-permissions)
|
(browser/initialize-dapp-permissions)
|
||||||
(extensions.registry/initialize)
|
(extensions.registry/initialize)
|
||||||
(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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)))))))
|
|
@ -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-name
|
channel-id
|
||||||
firebase.notifications.Android.Importance.Max)]
|
channel-name
|
||||||
|
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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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)))
|
||||||
|
|
|
@ -55,11 +55,11 @@
|
||||||
(re-frame/dispatch [:handle-universal-link url])))
|
(re-frame/dispatch [:handle-universal-link url])))
|
||||||
|
|
||||||
(fx/defn handle-browse [cofx 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})
|
{:browser/show-browser-selection url})
|
||||||
|
|
||||||
(fx/defn handle-public-chat [cofx public-chat]
|
(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}))
|
(chat/start-public-chat cofx public-chat {:navigation-reset? true}))
|
||||||
|
|
||||||
(fx/defn handle-view-profile [{:keys [db] :as cofx} public-key]
|
(fx/defn handle-view-profile [{:keys [db] :as cofx} public-key]
|
||||||
|
|
|
@ -2,34 +2,43 @@
|
||||||
(: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"
|
||||||
(is (= {:db {:accounts/accounts {"bd36cd64e2621b054a3b7464ff1b3c4c304880e7" {:address "bd36cd64e2621b054a3b7464ff1b3c4c304880e7"
|
(is (= {: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"}}
|
||||||
:account/account {:public-key nil}
|
:account/account {:public-key nil}
|
||||||
: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"}}
|
||||||
:account/account {:public-key nil}
|
:account/account {:public-key nil}
|
||||||
:push-notifications/stored {}}}
|
:push-notifications/stored {}}}
|
||||||
[:push-notification-opened {:from "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de"
|
[:push-notification-opened {:from "0x045db1fdb16c4721ddf32e892c5156d9c7a7445482b84ccd41131eb7970f9d623629d86763c5c2a542ae372815125c27eb73535d583d3285bdbfa16ba37f42e2de"
|
||||||
:to "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}])))))
|
:to "0x04d2e59a7501a7bc5bc8bf973a0ab95d06225e2b0f53d5f6be719d857c579bdc1b809bfbe0e8393343f9a5b63a9a90a1a58329c6d1c286d6929f01aaa5472ca9c2"}])))))
|
||||||
|
|
Loading…
Reference in New Issue