diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 4254941dd8..58360d5fb5 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -75,6 +75,7 @@
+
getPackages() {
StatusPackage statusPackage = new StatusPackage(RootUtil.isDeviceRooted());
+
List packages = new PackageList(this).getPackages();
packages.add(statusPackage);
packages.add(new ReactNativeDialogsPackage());
packages.add(new RNStatusKeycardPackage());
+ packages.add(new PushNotificationPackage());
return packages;
}
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 849681f8c3..8d5b9f10f1 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -1,5 +1,7 @@
PODS:
- boost-for-react-native (1.63.0)
+ - BVLinearGradient (2.5.6):
+ - React
- CocoaAsyncSocket (7.6.4)
- CocoaLibEvent (1.0.0)
- DoubleConversion (1.1.6)
@@ -373,6 +375,7 @@ PODS:
- Yoga (~> 1.14)
DEPENDENCIES:
+ - BVLinearGradient (from `../node_modules/react-native-linear-gradient`)
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
- FBReactNativeSpec (from `../node_modules/react-native/Libraries/FBReactNativeSpec`)
@@ -473,6 +476,8 @@ SPEC REPOS:
- YogaKit
EXTERNAL SOURCES:
+ BVLinearGradient:
+ :path: "../node_modules/react-native-linear-gradient"
DoubleConversion:
:podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
FBLazyVector:
@@ -588,6 +593,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c
+ BVLinearGradient: e3aad03778a456d77928f594a649e96995f1c872
CocoaAsyncSocket: 694058e7c0ed05a9e217d1b3c7ded962f4180845
CocoaLibEvent: 2fab71b8bd46dd33ddb959f7928ec5909f838e3f
DoubleConversion: 5805e889d232975c086db112ece9ed034df7a0b2
diff --git a/ios/StatusIm/AppDelegate.m b/ios/StatusIm/AppDelegate.m
index 0787c8ed96..f5e8b7b919 100644
--- a/ios/StatusIm/AppDelegate.m
+++ b/ios/StatusIm/AppDelegate.m
@@ -171,5 +171,18 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response
{
[RNCPushNotificationIOS didReceiveLocalNotification:notification];
}
+// Manage notifications while app is in the foreground
+- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler
+{
+ NSDictionary *userInfo = notification.request.content.userInfo;
+
+ NSString *notificationType = userInfo[@"notificationType"]; // check your notification type
+ if (![notificationType isEqual: @"local-notification"]) { // we silence all notifications which are not local
+ completionHandler(UNNotificationPresentationOptionNone);
+ return;
+ }
+
+ completionHandler(UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionBadge);
+}
@end
diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/LocalNotificationsService.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/LocalNotificationsService.java
new file mode 100644
index 0000000000..6de1503443
--- /dev/null
+++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/LocalNotificationsService.java
@@ -0,0 +1,20 @@
+package im.status.ethereum.module;
+
+import android.content.Intent;
+import android.os.Bundle;
+import com.facebook.react.HeadlessJsTaskService;
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.jstasks.HeadlessJsTaskConfig;
+import javax.annotation.Nullable;
+
+public class LocalNotificationsService extends HeadlessJsTaskService {
+
+ @Override
+ protected @Nullable HeadlessJsTaskConfig getTaskConfig(Intent intent) {
+ Bundle extras = intent.getExtras();
+ if (extras != null) {
+ return new HeadlessJsTaskConfig("LocalNotifications", Arguments.fromBundle(extras), 60000, true);
+ }
+ return null;
+ }
+}
diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java
index 157f8f3442..bc09459a4c 100644
--- a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java
+++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java
@@ -7,6 +7,7 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
+import android.os.Bundle;
import android.os.Build;
import android.os.Environment;
@@ -162,6 +163,18 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
newMessageSignalHandler.handleNewMessageSignal(jsonEvent);
}
}
+
+ if(eventType.equals("local-notifications")) {
+ Context ctx = this.getReactApplicationContext();
+ Intent intent = new Intent(ctx, LocalNotificationsService.class);
+ Bundle bundle = new Bundle();
+
+ bundle.putString("event", jsonEventString);
+ intent.putExtras(bundle);
+
+ ctx.startService(intent);
+ }
+
WritableMap params = Arguments.createMap();
params.putString("jsonEvent", jsonEventString);
this.getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("gethEvent", params);
@@ -1137,6 +1150,18 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
Statusgo.startWallet();
}
+ @ReactMethod
+ public void stopLocalNotifications() {
+ Log.d(TAG, "stopLocalNotifications");
+ Statusgo.stopLocalNotifications();
+ }
+
+ @ReactMethod
+ public void startLocalNotifications() {
+ Log.d(TAG, "startLocalNotifications");
+ Statusgo.startLocalNotifications();
+ }
+
@ReactMethod
public void setBlankPreviewFlag(final Boolean blankPreview) {
final SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this.reactContext);
diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotification.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotification.java
new file mode 100644
index 0000000000..fffca9330f
--- /dev/null
+++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotification.java
@@ -0,0 +1,110 @@
+package im.status.ethereum.pushnotifications;
+
+import android.app.Activity;
+import android.app.Application;
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.core.app.NotificationManagerCompat;
+
+import java.security.SecureRandom;
+
+import com.facebook.react.bridge.ActivityEventListener;
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.Callback;
+import com.facebook.react.bridge.Promise;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.bridge.WritableArray;
+import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.bridge.WritableMap;
+
+import im.status.ethereum.pushnotifications.PushNotificationJsDelivery;
+
+public class PushNotification extends ReactContextBaseJavaModule implements ActivityEventListener {
+ public static final String LOG_TAG = "PushNotification";
+
+ private final SecureRandom mRandomNumberGenerator = new SecureRandom();
+ private PushNotificationHelper pushNotificationHelper;
+ private PushNotificationJsDelivery delivery;
+
+ public PushNotification(ReactApplicationContext reactContext) {
+ super(reactContext);
+ reactContext.addActivityEventListener(this);
+ Application applicationContext = (Application) reactContext.getApplicationContext();
+
+ pushNotificationHelper = new PushNotificationHelper(applicationContext);
+
+ delivery = new PushNotificationJsDelivery(reactContext);
+ }
+
+ @Override
+ public String getName() {
+ return "PushNotification";
+ }
+
+ // removed @Override temporarily just to get it working on different versions of RN
+ public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
+ onActivityResult(requestCode, resultCode, data);
+ }
+
+ // removed @Override temporarily just to get it working on different versions of RN
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ // Ignored, required to implement ActivityEventListener for RN 0.33
+ }
+
+ private Bundle getBundleFromIntent(Intent intent) {
+ Bundle bundle = null;
+ if (intent.hasExtra("notification")) {
+ bundle = intent.getBundleExtra("notification");
+ } else if (intent.hasExtra("google.message_id")) {
+ bundle = new Bundle();
+
+ bundle.putBundle("data", intent.getExtras());
+ }
+
+ if(null != bundle && !bundle.getBoolean("foreground", false) && !bundle.containsKey("userInteraction")) {
+ bundle.putBoolean("userInteraction", true);
+ }
+
+ return bundle;
+ }
+
+ @Override
+ public void onNewIntent(Intent intent) {
+ Bundle bundle = this.getBundleFromIntent(intent);
+ if (bundle != null) {
+ delivery.notifyNotification(bundle);
+ }
+ }
+
+ @ReactMethod
+ /**
+ * Creates a channel if it does not already exist. Returns whether the channel was created.
+ */
+ public void createChannel(ReadableMap channelInfo, Callback callback) {
+ boolean created = pushNotificationHelper.createChannel(channelInfo);
+
+ if(callback != null) {
+ callback.invoke(created);
+ }
+ }
+
+ @ReactMethod
+ public void presentLocalNotification(ReadableMap details) {
+ Bundle bundle = Arguments.toBundle(details);
+ // If notification ID is not provided by the user, generate one at random
+ if (bundle.getString("id") == null) {
+ bundle.putString("id", String.valueOf(mRandomNumberGenerator.nextInt()));
+ }
+ pushNotificationHelper.sendToNotificationCentre(bundle);
+ }
+}
diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationActions.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationActions.java
new file mode 100644
index 0000000000..88dbcec836
--- /dev/null
+++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationActions.java
@@ -0,0 +1,90 @@
+package im.status.ethereum.pushnotifications;
+
+import android.os.Build;
+import android.app.Application;
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import com.facebook.react.ReactApplication;
+import com.facebook.react.ReactInstanceManager;
+import com.facebook.react.bridge.ReactContext;
+
+
+import im.status.ethereum.pushnotifications.PushNotificationJsDelivery;
+import static im.status.ethereum.pushnotifications.PushNotification.LOG_TAG;
+
+public class PushNotificationActions extends BroadcastReceiver {
+ @Override
+ public void onReceive(final Context context, Intent intent) {
+ String intentActionPrefix = context.getPackageName() + ".ACTION_";
+
+ Log.i(LOG_TAG, "PushNotificationBootEventReceiver loading scheduled notifications");
+
+ if (null == intent.getAction() || !intent.getAction().startsWith(intentActionPrefix)) {
+ return;
+ }
+
+ final Bundle bundle = intent.getBundleExtra("notification");
+
+ // Dismiss the notification popup.
+ NotificationManager manager = (NotificationManager) context.getSystemService(context.NOTIFICATION_SERVICE);
+ int notificationID = Integer.parseInt(bundle.getString("id"));
+
+ boolean autoCancel = bundle.getBoolean("autoCancel", true);
+
+ if(autoCancel) {
+ if (bundle.containsKey("tag")) {
+ String tag = bundle.getString("tag");
+ manager.cancel(tag, notificationID);
+ } else {
+ manager.cancel(notificationID);
+ }
+ }
+
+ boolean invokeApp = bundle.getBoolean("invokeApp", true);
+
+ // Notify the action.
+ if(invokeApp) {
+ PushNotificationHelper helper = new PushNotificationHelper((Application) context.getApplicationContext());
+
+ helper.invokeApp(bundle);
+ } else {
+
+ // We need to run this on the main thread, as the React code assumes that is true.
+ // Namely, DevServerHelper constructs a Handler() without a Looper, which triggers:
+ // "Can't create handler inside thread that has not called Looper.prepare()"
+ Handler handler = new Handler(Looper.getMainLooper());
+ handler.post(new Runnable() {
+ public void run() {
+ // Construct and load our normal React JS code bundle
+ final ReactInstanceManager mReactInstanceManager = ((ReactApplication) context.getApplicationContext()).getReactNativeHost().getReactInstanceManager();
+ ReactContext context = mReactInstanceManager.getCurrentReactContext();
+ PushNotificationJsDelivery delivery = new PushNotificationJsDelivery(context);
+ // If it's constructed, send a notification
+ if (context != null) {
+ delivery.notifyNotificationAction(bundle);
+ } else {
+ // Otherwise wait for construction, then send the notification
+ mReactInstanceManager.addReactInstanceEventListener(new ReactInstanceManager.ReactInstanceEventListener() {
+ public void onReactContextInitialized(ReactContext context) {
+ PushNotificationJsDelivery delivery = new PushNotificationJsDelivery(context);
+ delivery.notifyNotificationAction(bundle);
+ mReactInstanceManager.removeReactInstanceEventListener(this);
+ }
+ });
+ if (!mReactInstanceManager.hasStartedCreatingInitialContext()) {
+ // Construct it in the background
+ mReactInstanceManager.createReactContextInBackground();
+ }
+ }
+ }
+ });
+ }
+ }
+}
diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationHelper.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationHelper.java
new file mode 100644
index 0000000000..9f7f94697f
--- /dev/null
+++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationHelper.java
@@ -0,0 +1,549 @@
+// https://github.com/zo0r/react-native-push-notification/blob/bedc8f646aab67d594f291449fbfa24e07b64fe8/android/src/main/java/com/dieam/reactnativepushnotification/modules/RNPushNotificationHelper.java Copy-Paste with removed firebase
+package im.status.ethereum.pushnotifications;
+
+import android.app.ActivityManager;
+import android.app.ActivityManager.RunningAppProcessInfo;
+import android.app.AlarmManager;
+import android.app.Application;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.media.AudioAttributes;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.service.notification.StatusBarNotification;
+import android.util.Log;
+
+import androidx.annotation.RequiresApi;
+import androidx.core.app.NotificationCompat;
+
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.bridge.WritableArray;
+import com.facebook.react.bridge.WritableMap;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.List;
+import java.util.Map;
+
+import static im.status.ethereum.pushnotifications.PushNotification.LOG_TAG;
+
+public class PushNotificationHelper {
+
+ private Context context;
+
+ private static final long DEFAULT_VIBRATION = 300L;
+ private static final String CHANNEL_ID = "status-im-notifications";
+
+ public PushNotificationHelper(Application context) {
+ this.context = context;
+ }
+
+ private NotificationManager notificationManager() {
+ return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ public void invokeApp(Bundle bundle) {
+ String packageName = context.getPackageName();
+ Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
+ String className = launchIntent.getComponent().getClassName();
+
+ try {
+ Class> activityClass = Class.forName(className);
+ Intent activityIntent = new Intent(context, activityClass);
+
+ if(bundle != null) {
+ activityIntent.putExtra("notification", bundle);
+ }
+
+ activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ context.startActivity(activityIntent);
+ } catch(Exception e) {
+ Log.e(LOG_TAG, "Class not found", e);
+ return;
+ }
+ }
+
+ public void sendToNotificationCentre(final Bundle bundle) {
+ PushNotificationPicturesAggregator aggregator = new PushNotificationPicturesAggregator(new PushNotificationPicturesAggregator.Callback() {
+ public void call(Bitmap largeIconImage, Bitmap bigPictureImage) {
+ sendToNotificationCentreWithPicture(bundle, largeIconImage, bigPictureImage);
+ }
+ });
+
+ aggregator.setLargeIconUrl(context, bundle.getString("largeIconUrl"));
+ aggregator.setBigPictureUrl(context, bundle.getString("bigPictureUrl"));
+ }
+
+ public void sendToNotificationCentreWithPicture(final Bundle bundle, Bitmap largeIconBitmap, Bitmap bigPictureBitmap) {
+ try {
+ Class intentClass = getMainActivityClass();
+ if (intentClass == null) {
+ Log.e(LOG_TAG, "No activity class found for the notification");
+ return;
+ }
+
+ if (bundle.getString("message") == null) {
+ // this happens when a 'data' notification is received - we do not synthesize a local notification in this case
+ Log.d(LOG_TAG, "Ignore this message if you sent data-only notification. Cannot send to notification centre because there is no 'message' field in: " + bundle);
+ return;
+ }
+
+ String notificationIdString = bundle.getString("id");
+ if (notificationIdString == null) {
+ Log.e(LOG_TAG, "No notification ID specified for the notification");
+ return;
+ }
+
+ Resources res = context.getResources();
+ String packageName = context.getPackageName();
+
+ String title = bundle.getString("title");
+ if (title == null) {
+ ApplicationInfo appInfo = context.getApplicationInfo();
+ title = context.getPackageManager().getApplicationLabel(appInfo).toString();
+ }
+
+ int priority = NotificationCompat.PRIORITY_HIGH;
+ final String priorityString = bundle.getString("priority");
+
+ if (priorityString != null) {
+ switch (priorityString.toLowerCase()) {
+ case "max":
+ priority = NotificationCompat.PRIORITY_MAX;
+ break;
+ case "high":
+ priority = NotificationCompat.PRIORITY_HIGH;
+ break;
+ case "low":
+ priority = NotificationCompat.PRIORITY_LOW;
+ break;
+ case "min":
+ priority = NotificationCompat.PRIORITY_MIN;
+ break;
+ case "default":
+ priority = NotificationCompat.PRIORITY_DEFAULT;
+ break;
+ default:
+ priority = NotificationCompat.PRIORITY_HIGH;
+ }
+ }
+
+ int visibility = NotificationCompat.VISIBILITY_PRIVATE;
+ final String visibilityString = bundle.getString("visibility");
+
+ if (visibilityString != null) {
+ switch (visibilityString.toLowerCase()) {
+ case "private":
+ visibility = NotificationCompat.VISIBILITY_PRIVATE;
+ break;
+ case "public":
+ visibility = NotificationCompat.VISIBILITY_PUBLIC;
+ break;
+ case "secret":
+ visibility = NotificationCompat.VISIBILITY_SECRET;
+ break;
+ default:
+ visibility = NotificationCompat.VISIBILITY_PRIVATE;
+ }
+ }
+
+ String channel_id = bundle.getString("channelId");
+
+ if(channel_id == null) {
+ channel_id = this.getNotificationDefaultChannelId();
+ }
+
+ NotificationCompat.Builder notification = new NotificationCompat.Builder(context, channel_id)
+ .setContentTitle(title)
+ .setTicker(bundle.getString("ticker"))
+ .setVisibility(visibility)
+ .setPriority(priority)
+ .setAutoCancel(bundle.getBoolean("autoCancel", true))
+ .setOnlyAlertOnce(bundle.getBoolean("onlyAlertOnce", false));
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // API 24 and higher
+ // Restore showing timestamp on Android 7+
+ // Source: https://developer.android.com/reference/android/app/Notification.Builder.html#setShowWhen(boolean)
+ boolean showWhen = bundle.getBoolean("showWhen", true);
+
+ notification.setShowWhen(showWhen);
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // API 26 and higher
+ // Changing Default mode of notification
+ notification.setDefaults(Notification.DEFAULT_LIGHTS);
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { // API 20 and higher
+ String group = bundle.getString("group");
+
+ if (group != null) {
+ notification.setGroup(group);
+ }
+
+ if (bundle.containsKey("groupSummary") || bundle.getBoolean("groupSummary")) {
+ notification.setGroupSummary(bundle.getBoolean("groupSummary"));
+ }
+ }
+
+ String numberString = bundle.getString("number");
+
+ if (numberString != null) {
+ notification.setNumber(Integer.parseInt(numberString));
+ }
+
+ // Small icon
+ int smallIconResId = 0;
+
+ String smallIcon = bundle.getString("smallIcon");
+
+ if (smallIcon != null && !smallIcon.isEmpty()) {
+ smallIconResId = res.getIdentifier(smallIcon, "mipmap", packageName);
+ } else if(smallIcon == null) {
+ smallIconResId = res.getIdentifier("ic_stat_notify_status", "drawable", packageName);
+ }
+
+ if (smallIconResId == 0) {
+ smallIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName);
+
+ if (smallIconResId == 0) {
+ smallIconResId = android.R.drawable.ic_dialog_info;
+ }
+ }
+
+ notification.setSmallIcon(smallIconResId);
+
+ // Large icon
+ if(largeIconBitmap == null) {
+ int largeIconResId = 0;
+
+ String largeIcon = bundle.getString("largeIcon");
+
+ if (largeIcon != null && !largeIcon.isEmpty()) {
+ largeIconResId = res.getIdentifier(largeIcon, "mipmap", packageName);
+ } else if(largeIcon == null) {
+ largeIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName);
+ }
+
+ // Before Lolipop there was no large icon for notifications.
+ if (largeIconResId != 0 && (largeIcon != null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)) {
+ largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId);
+ }
+ }
+
+ if (largeIconBitmap != null){
+ notification.setLargeIcon(largeIconBitmap);
+ }
+
+ String message = bundle.getString("message");
+
+ notification.setContentText(message);
+
+ String subText = bundle.getString("subText");
+
+ if (subText != null) {
+ notification.setSubText(subText);
+ }
+
+ String bigText = bundle.getString("bigText");
+
+ if (bigText == null) {
+ bigText = message;
+ }
+
+ NotificationCompat.Style style;
+
+ if(bigPictureBitmap != null) {
+ style = new NotificationCompat.BigPictureStyle()
+ .bigPicture(bigPictureBitmap)
+ .setBigContentTitle(title)
+ .setSummaryText(message);
+ } else {
+ style = new NotificationCompat.BigTextStyle().bigText(bigText);
+ }
+
+ notification.setStyle(style);
+
+ Intent intent = new Intent(context, intentClass);
+ intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ bundle.putBoolean("foreground", this.isApplicationInForeground());
+ bundle.putBoolean("userInteraction", true);
+ intent.putExtra("notification", bundle);
+
+ Uri soundUri = null;
+
+ if (!bundle.containsKey("playSound") || bundle.getBoolean("playSound")) {
+ String soundName = bundle.getString("soundName");
+
+ if (soundName == null) {
+ soundName = "default";
+ }
+
+ soundUri = getSoundUri(soundName);
+
+ notification.setSound(soundUri);
+ }
+
+ if (soundUri == null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ notification.setSound(null);
+ }
+
+ if (bundle.containsKey("ongoing") || bundle.getBoolean("ongoing")) {
+ notification.setOngoing(bundle.getBoolean("ongoing"));
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ notification.setCategory(NotificationCompat.CATEGORY_CALL);
+
+ String color = bundle.getString("color");
+ int defaultColor = -1;
+ if (color != null) {
+ notification.setColor(Color.parseColor(color));
+ } else if (defaultColor != -1) {
+ notification.setColor(defaultColor);
+ }
+ }
+
+ int notificationID = Integer.parseInt(notificationIdString);
+
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, notificationID, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+
+ NotificationManager notificationManager = notificationManager();
+
+ long[] vibratePattern = new long[]{0};
+
+ if (!bundle.containsKey("vibrate") || bundle.getBoolean("vibrate")) {
+ long vibration = bundle.containsKey("vibration") ? (long) bundle.getDouble("vibration") : DEFAULT_VIBRATION;
+ if (vibration == 0)
+ vibration = DEFAULT_VIBRATION;
+
+ vibratePattern = new long[]{0, vibration};
+
+ notification.setVibrate(vibratePattern);
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ // Define the shortcutId
+ String shortcutId = bundle.getString("shortcutId");
+
+ if (shortcutId != null) {
+ notification.setShortcutId(shortcutId);
+ }
+
+ Long timeoutAfter = (long) bundle.getDouble("timeoutAfter");
+
+ if (timeoutAfter != null && timeoutAfter >= 0) {
+ notification.setTimeoutAfter(timeoutAfter);
+ }
+ }
+
+ Long when = (long) bundle.getDouble("when");
+
+ if (when != null && when >= 0) {
+ notification.setWhen(when);
+ }
+
+ notification.setUsesChronometer(bundle.getBoolean("usesChronometer", false));
+ notification.setChannelId(channel_id);
+ notification.setContentIntent(pendingIntent);
+
+ JSONArray actionsArray = null;
+ try {
+ actionsArray = bundle.getString("actions") != null ? new JSONArray(bundle.getString("actions")) : null;
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, "Exception while converting actions to JSON object.", e);
+ }
+
+ if (actionsArray != null) {
+ // No icon for now. The icon value of 0 shows no icon.
+ int icon = 0;
+
+ // Add button for each actions.
+ for (int i = 0; i < actionsArray.length(); i++) {
+ String action;
+ try {
+ action = actionsArray.getString(i);
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, "Exception while getting action from actionsArray.", e);
+ continue;
+ }
+
+
+ Intent actionIntent = new Intent(context, PushNotificationActions.class);
+ actionIntent.setAction(packageName + ".ACTION_" + i);
+
+ actionIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+
+ // Add "action" for later identifying which button gets pressed.
+ bundle.putString("action", action);
+ actionIntent.putExtra("notification", bundle);
+ actionIntent.setPackage(packageName);
+
+ PendingIntent pendingActionIntent = PendingIntent.getBroadcast(context, notificationID, actionIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ notification.addAction(new NotificationCompat.Action.Builder(icon, action, pendingActionIntent).build());
+ } else {
+ notification.addAction(icon, action, pendingActionIntent);
+ }
+ }
+ }
+
+
+ if (!(this.isApplicationInForeground() && bundle.getBoolean("ignoreInForeground"))) {
+ Notification info = notification.build();
+ info.defaults |= Notification.DEFAULT_LIGHTS;
+
+ if (bundle.containsKey("tag")) {
+ String tag = bundle.getString("tag");
+ notificationManager.notify(tag, notificationID, info);
+ } else {
+ notificationManager.notify(notificationID, info);
+ }
+ }
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "failed to send push notification", e);
+ }
+ }
+
+ private boolean checkOrCreateChannel(NotificationManager manager, String channel_id, String channel_name, String channel_description, Uri soundUri, int importance, long[] vibratePattern, boolean showBadge) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
+ return false;
+ if (manager == null)
+ return false;
+
+ NotificationChannel channel = manager.getNotificationChannel(channel_id);
+
+ if (
+ channel == null && channel_name != null && channel_description != null ||
+ channel != null &&
+ (
+ channel_name != null && !channel.getName().equals(channel_name) ||
+ channel_description != null && !channel.getDescription().equals(channel_description)
+ )
+ ) {
+ // If channel doesn't exist create a new one.
+ // If channel name or description is updated then update the existing channel.
+ channel = new NotificationChannel(channel_id, channel_name, importance);
+
+ channel.setDescription(channel_description);
+ channel.enableLights(true);
+ channel.enableVibration(vibratePattern != null);
+ channel.setVibrationPattern(vibratePattern);
+ channel.setShowBadge(showBadge);
+
+ if (soundUri != null) {
+ AudioAttributes audioAttributes = new AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION)
+ .build();
+
+ channel.setSound(soundUri, audioAttributes);
+ } else {
+ channel.setSound(null, null);
+ }
+
+ manager.createNotificationChannel(channel);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public boolean createChannel(ReadableMap channelInfo) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
+ return false;
+
+ String channelId = channelInfo.getString("channelId");
+ String channelName = channelInfo.getString("channelName");
+ String channelDescription = channelInfo.hasKey("channelDescription") ? channelInfo.getString("channelDescription") : "";
+ String soundName = channelInfo.hasKey("soundName") ? channelInfo.getString("soundName") : "default";
+ int importance = channelInfo.hasKey("importance") ? channelInfo.getInt("importance") : 4;
+ boolean vibrate = channelInfo.hasKey("vibrate") && channelInfo.getBoolean("vibrate");
+ long[] vibratePattern = vibrate ? new long[] { DEFAULT_VIBRATION } : null;
+ boolean showBadge = channelInfo.hasKey("showBadge") && channelInfo.getBoolean("showBadge");
+
+ NotificationManager manager = notificationManager();
+
+ Uri soundUri = getSoundUri(soundName);
+
+ return checkOrCreateChannel(manager, channelId, channelName, channelDescription, soundUri, importance, vibratePattern, showBadge);
+ }
+
+ public String getNotificationDefaultChannelId() {
+ return this.CHANNEL_ID;
+ }
+
+ private Uri getSoundUri(String soundName) {
+ if (soundName == null || "default".equalsIgnoreCase(soundName)) {
+ return RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
+ } else {
+
+ // sound name can be full filename, or just the resource name.
+ // So the strings 'my_sound.mp3' AND 'my_sound' are accepted
+ // The reason is to make the iOS and android javascript interfaces compatible
+
+ int resId;
+ if (context.getResources().getIdentifier(soundName, "raw", context.getPackageName()) != 0) {
+ resId = context.getResources().getIdentifier(soundName, "raw", context.getPackageName());
+ } else {
+ soundName = soundName.substring(0, soundName.lastIndexOf('.'));
+ resId = context.getResources().getIdentifier(soundName, "raw", context.getPackageName());
+ }
+
+ return Uri.parse("android.resource://" + context.getPackageName() + "/" + resId);
+ }
+ }
+
+ public Class getMainActivityClass() {
+ String packageName = context.getPackageName();
+ Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
+ String className = launchIntent.getComponent().getClassName();
+ try {
+ return Class.forName(className);
+ } catch (ClassNotFoundException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public boolean isApplicationInForeground() {
+ ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ List processInfos = activityManager.getRunningAppProcesses();
+ if (processInfos != null) {
+ for (RunningAppProcessInfo processInfo : processInfos) {
+ if (processInfo.processName.equals(context.getPackageName())
+ && processInfo.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND
+ && processInfo.pkgList.length > 0) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationJsDelivery.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationJsDelivery.java
new file mode 100644
index 0000000000..585dca43d5
--- /dev/null
+++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationJsDelivery.java
@@ -0,0 +1,86 @@
+package im.status.ethereum.pushnotifications;
+
+import android.os.Build;
+import android.app.Application;
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import com.facebook.react.bridge.ReactContext;
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.modules.core.DeviceEventManagerModule;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.Set;
+
+import static im.status.ethereum.pushnotifications.PushNotification.LOG_TAG;
+
+class PushNotificationJsDelivery {
+
+ private ReactContext reactContext;
+
+ PushNotificationJsDelivery(ReactContext context){
+ reactContext = context;
+ }
+
+ String convertJSON(Bundle bundle) {
+ try {
+ JSONObject json = convertJSONObject(bundle);
+ return json.toString();
+ } catch (JSONException e) {
+ return null;
+ }
+ }
+
+ // a Bundle is not a map, so we have to convert it explicitly
+ private JSONObject convertJSONObject(Bundle bundle) throws JSONException {
+ JSONObject json = new JSONObject();
+ Set keys = bundle.keySet();
+ for (String key : keys) {
+ Object value = bundle.get(key);
+ if (value instanceof Bundle) {
+ json.put(key, convertJSONObject((Bundle)value));
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ json.put(key, JSONObject.wrap(value));
+ } else {
+ json.put(key, value);
+ }
+ }
+ return json;
+ }
+
+ void notifyNotification(Bundle bundle) {
+ String bundleString = convertJSON(bundle);
+
+ WritableMap params = Arguments.createMap();
+ params.putString("dataJSON", bundleString);
+
+ sendEvent("remoteNotificationReceived", params);
+ }
+
+ void notifyNotificationAction(Bundle bundle) {
+ String bundleString = convertJSON(bundle);
+
+ WritableMap params = Arguments.createMap();
+ params.putString("dataJSON", bundleString);
+
+ sendEvent("notificationActionReceived", params);
+ }
+
+ void sendEvent(String eventName, Object params) {
+ if (reactContext.hasActiveCatalystInstance()) {
+ reactContext
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
+ .emit(eventName, params);
+ }
+ }
+
+}
diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationPackage.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationPackage.java
new file mode 100644
index 0000000000..0a4391fdd8
--- /dev/null
+++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationPackage.java
@@ -0,0 +1,27 @@
+package im.status.ethereum.pushnotifications;
+
+import im.status.ethereum.pushnotifications.PushNotification;
+import com.facebook.react.ReactPackage;
+import com.facebook.react.bridge.JavaScriptModule;
+import com.facebook.react.bridge.NativeModule;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.uimanager.ViewManager;
+
+import java.util.Collections;
+import java.util.List;
+
+public class PushNotificationPackage implements ReactPackage {
+ @Override
+ public List createNativeModules(ReactApplicationContext reactContext) {
+ return Collections.singletonList(new PushNotification(reactContext));
+ }
+
+ public List> createJSModules() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List createViewManagers(ReactApplicationContext reactContext) {
+ return Collections.emptyList();
+ }
+}
diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationPicturesAggregator.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationPicturesAggregator.java
new file mode 100644
index 0000000000..f34c28cbfd
--- /dev/null
+++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationPicturesAggregator.java
@@ -0,0 +1,136 @@
+package im.status.ethereum.pushnotifications;
+
+import androidx.annotation.Nullable;
+import com.facebook.common.executors.CallerThreadExecutor;
+import com.facebook.common.references.CloseableReference;
+import com.facebook.datasource.DataSource;
+import com.facebook.drawee.backends.pipeline.Fresco;
+import com.facebook.imagepipeline.common.Priority;
+import com.facebook.imagepipeline.core.ImagePipeline;
+import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
+import com.facebook.imagepipeline.image.CloseableImage;
+import com.facebook.imagepipeline.request.ImageRequest;
+import com.facebook.imagepipeline.request.ImageRequestBuilder;
+
+import android.util.Log;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static im.status.ethereum.pushnotifications.PushNotification.LOG_TAG;
+
+public class PushNotificationPicturesAggregator {
+ interface Callback {
+ public void call(Bitmap largeIconImage, Bitmap bigPictureImage);
+ }
+
+ private AtomicInteger count = new AtomicInteger(0);
+
+ private Bitmap largeIconImage;
+ private Bitmap bigPictureImage;
+
+ private Callback callback;
+
+ public PushNotificationPicturesAggregator(Callback callback) {
+ this.callback = callback;
+ }
+
+ public void setBigPicture(Bitmap bitmap) {
+ this.bigPictureImage = bitmap;
+ this.finished();
+ }
+
+ public void setBigPictureUrl(Context context, String url) {
+ if(null == url) {
+ this.setBigPicture(null);
+ return;
+ }
+
+ Uri uri = null;
+
+ try {
+ uri = Uri.parse(url);
+ } catch(Exception ex) {
+ Log.e(LOG_TAG, "Failed to parse bigPictureUrl", ex);
+ this.setBigPicture(null);
+ return;
+ }
+
+ final PushNotificationPicturesAggregator aggregator = this;
+
+ this.downloadRequest(context, uri, new BaseBitmapDataSubscriber() {
+ @Override
+ public void onNewResultImpl(@Nullable Bitmap bitmap) {
+ aggregator.setBigPicture(bitmap);
+ }
+
+ @Override
+ public void onFailureImpl(DataSource dataSource) {
+ aggregator.setBigPicture(null);
+ }
+ });
+ }
+
+ public void setLargeIcon(Bitmap bitmap) {
+ this.largeIconImage = bitmap;
+ this.finished();
+ }
+
+ public void setLargeIconUrl(Context context, String url) {
+ if(null == url) {
+ this.setLargeIcon(null);
+ return;
+ }
+
+ Uri uri = null;
+
+ try {
+ uri = Uri.parse(url);
+ } catch(Exception ex) {
+ Log.e(LOG_TAG, "Failed to parse largeIconUrl", ex);
+ this.setLargeIcon(null);
+ return;
+ }
+
+ final PushNotificationPicturesAggregator aggregator = this;
+
+ this.downloadRequest(context, uri, new BaseBitmapDataSubscriber() {
+ @Override
+ public void onNewResultImpl(@Nullable Bitmap bitmap) {
+ aggregator.setLargeIcon(bitmap);
+ }
+
+ @Override
+ public void onFailureImpl(DataSource dataSource) {
+ aggregator.setLargeIcon(null);
+ }
+ });
+ }
+
+ private void downloadRequest(Context context, Uri uri, BaseBitmapDataSubscriber subscriber) {
+ ImageRequest imageRequest = ImageRequestBuilder
+ .newBuilderWithSource(uri)
+ .setRequestPriority(Priority.HIGH)
+ .setLowestPermittedRequestLevel(ImageRequest.RequestLevel.FULL_FETCH)
+ .build();
+
+ if(!Fresco.hasBeenInitialized()) {
+ Fresco.initialize(context);
+ }
+
+ DataSource> dataSource = Fresco.getImagePipeline().fetchDecodedImage(imageRequest, context);
+
+ dataSource.subscribe(subscriber, CallerThreadExecutor.getInstance());
+ }
+
+ private void finished() {
+ synchronized(this.count) {
+ int val = this.count.incrementAndGet();
+
+ if(val >= 2 && this.callback != null) {
+ this.callback.call(this.largeIconImage, this.bigPictureImage);
+ }
+ }
+ }
+}
diff --git a/modules/react-native-status/ios/RCTStatus/RCTStatus.m b/modules/react-native-status/ios/RCTStatus/RCTStatus.m
index 9048ebd119..5d5dbd54ec 100644
--- a/modules/react-native-status/ios/RCTStatus/RCTStatus.m
+++ b/modules/react-native-status/ios/RCTStatus/RCTStatus.m
@@ -754,6 +754,22 @@ RCT_EXPORT_METHOD(startWallet) {
StatusgoStartWallet();
}
+RCT_EXPORT_METHOD(stopLocalNotifications) {
+#if DEBUG
+ NSLog(@"StopLocalNotifications() method called");
+#endif
+StatusgoStopLocalNotifications();
+}
+
+RCT_EXPORT_METHOD(startLocalNotifications) {
+#if DEBUG
+ NSLog(@"StartLocalNotifications() method called");
+#endif
+StatusgoStartLocalNotifications();
+}
+
+
+
RCT_EXPORT_METHOD(setBlankPreviewFlag:(BOOL *)newValue)
{
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
diff --git a/src/mocks/js_dependencies.cljs b/src/mocks/js_dependencies.cljs
index f8b57679e6..4154a9b3a4 100644
--- a/src/mocks/js_dependencies.cljs
+++ b/src/mocks/js_dependencies.cljs
@@ -18,6 +18,7 @@
(def react-native
(clj->js {:NativeModules {:RNGestureHandlerModule {:Direction (fn [])}
+ :PushNotifications {}
:ReanimatedModule {:configureProps (fn [])}}
:View {}
diff --git a/src/status_im/core.cljs b/src/status_im/core.cljs
index c0288871a8..84f4b91037 100644
--- a/src/status_im/core.cljs
+++ b/src/status_im/core.cljs
@@ -11,6 +11,7 @@
[reagent.core :as reagent]
[reagent.impl.batching :as batching]
[status-im.i18n :as i18n]
+ [status-im.notifications.local :as notifications]
[status-im.native-module.core :as status]
[status-im.ui.components.react :as react]
[status-im.ui.screens.views :as views]
@@ -87,4 +88,7 @@
(when platform/android?
(status/set-soft-input-mode status/adjust-resize))
(.registerComponent ^js app-registry "StatusIm" #(reagent/reactify-component root))
+ (notifications/listen-notifications)
+ (when platform/android?
+ (.registerHeadlessTask ^js app-registry "LocalNotifications" notifications/handle))
(snoopy/subscribe!))
diff --git a/src/status_im/ethereum/json_rpc.cljs b/src/status_im/ethereum/json_rpc.cljs
index 0996ebcdd4..9b20c4507c 100644
--- a/src/status_im/ethereum/json_rpc.cljs
+++ b/src/status_im/ethereum/json_rpc.cljs
@@ -109,6 +109,8 @@
"wakuext_enablePushNotificationsBlockMentions" {}
"wakuext_disablePushNotificationsBlockMentions" {}
"status_chats" {}
+ "localnotifications_switchWalletNotifications" {}
+ "localnotifications_notificationPreferences" {}
"wallet_getTransfers" {}
"wallet_getTokensBalances" {}
"wallet_getBlocksByAddress" {}
diff --git a/src/status_im/ethereum/tokens.cljs b/src/status_im/ethereum/tokens.cljs
index 049eb05a58..18a8dcc4b4 100644
--- a/src/status_im/ethereum/tokens.cljs
+++ b/src/status_im/ethereum/tokens.cljs
@@ -790,7 +790,7 @@
{:name "Status Test Token"
:symbol :STT
:decimals 18
- :address "0x43d5adc3b49130a575ae6e4b00dfa4bc55c71621"}])
+ :address "0xc55cf4b03948d7ebc8b9e8bad92643703811d162"}])
:xdai
(resolve-icons :xdai
@@ -801,6 +801,18 @@
:custom []})
+(defn normalize-chain [tokens]
+ (reduce (fn [acc {:keys [address] :as token}]
+ (assoc acc address token))
+ {}
+ tokens))
+
+(def all-tokens-normalized
+ (reduce-kv (fn [m k v]
+ (assoc m k (normalize-chain v)))
+ {}
+ all-default-tokens))
+
(defn nfts-for [all-tokens]
(filter :nft? (vals all-tokens)))
diff --git a/src/status_im/multiaccounts/login/core.cljs b/src/status_im/multiaccounts/login/core.cljs
index a81d2ae609..826801248f 100644
--- a/src/status_im/multiaccounts/login/core.cljs
+++ b/src/status_im/multiaccounts/login/core.cljs
@@ -36,6 +36,11 @@
(fn [[key-uid account-data hashed-password]]
(status/login key-uid account-data hashed-password)))
+(re-frame/reg-fx
+ ::enable-local-notifications
+ (fn []
+ (status/start-local-notifications)))
+
(defn rpc->accounts [accounts]
(reduce (fn [acc {:keys [chat type wallet] :as account}]
(if chat
@@ -58,8 +63,10 @@
[{:keys [db] :as cofx} accounts custom-tokens favourites new-account?]
(fx/merge
cofx
- {:db (assoc db :multiaccount/accounts
- (rpc->accounts accounts))}
+ {:db (assoc db :multiaccount/accounts
+ (rpc->accounts accounts))
+ ;; NOTE: Local notifications should be enabled only after wallet was started
+ ::enable-local-notifications nil}
(wallet/initialize-tokens custom-tokens)
(wallet/initialize-favourites favourites)
(wallet/update-balances nil new-account?)
@@ -180,12 +187,12 @@
(fx/defn get-settings-callback
{:events [::get-settings-callback]}
[{:keys [db] :as cofx} settings]
- (let [{:keys [notifications-enabled?]
+ (let [{:keys [notifications-enabled?]
:networks/keys [current-network networks]
- :as settings}
+ :as settings}
(data-store.settings/rpc->settings settings)
multiaccount (dissoc settings :networks/current-network :networks/networks)
- network-id (str (get-in networks [current-network :config :NetworkId]))]
+ network-id (str (get-in networks [current-network :config :NetworkId]))]
(fx/merge cofx
(cond-> {:db (-> db
(dissoc :multiaccounts/login)
@@ -242,6 +249,7 @@
:on-success #(re-frame/dispatch [::protocol/initialize-protocol {:mailservers (or % [])}])}
{:method "settings_getSettings"
:on-success #(re-frame/dispatch [::get-settings-callback %])}]}
+ (notifications/load-notification-preferences)
(when save-password?
(keychain/save-user-password key-uid password))
(keychain/save-auth-method key-uid (or new-auth-method auth-method keychain/auth-method-none)))))
diff --git a/src/status_im/multiaccounts/logout/core.cljs b/src/status_im/multiaccounts/logout/core.cljs
index 56ee559d5f..497236764e 100644
--- a/src/status_im/multiaccounts/logout/core.cljs
+++ b/src/status_im/multiaccounts/logout/core.cljs
@@ -16,6 +16,7 @@
(fx/merge cofx
{::logout nil
::multiaccounts/webview-debug-changed false
+ ::disable-local-notifications nil
:keychain/clear-user-password key-uid
::init/open-multiaccounts #(re-frame/dispatch [::init/initialize-multiaccounts % {:logout? logout?}])}
(notifications/logout-disable)
@@ -24,6 +25,11 @@
(chaos-mode/stop-checking)
(init/initialize-app-db))))
+(re-frame/reg-fx
+ ::disable-local-notifications
+ (fn []
+ (status/stop-local-notifications)))
+
(fx/defn logout
{:events [:logout :multiaccounts.logout.ui/logout-confirmed]}
[cofx]
diff --git a/src/status_im/native_module/core.cljs b/src/status_im/native_module/core.cljs
index 9ecb1b476d..3f630d9c96 100644
--- a/src/status_im/native_module/core.cljs
+++ b/src/status_im/native_module/core.cljs
@@ -271,6 +271,14 @@
(log/debug "[native-module] start-wallet")
(.startWallet ^js (status)))
+(defn stop-local-notifications []
+ (log/debug "[native-module] stop-local-notifications")
+ (.stopLocalNotifications ^js (status)))
+
+(defn start-local-notifications []
+ (log/debug "[native-module] start-local-notifications")
+ (.startLocalNotifications ^js (status)))
+
(defn set-blank-preview-flag [flag]
(log/debug "[native-module] set-blank-preview-flag")
(.setBlankPreviewFlag ^js (status) flag))
diff --git a/src/status_im/navigation.cljs b/src/status_im/navigation.cljs
index 747faabb6a..5393842304 100644
--- a/src/status_im/navigation.cljs
+++ b/src/status_im/navigation.cljs
@@ -28,15 +28,19 @@
(log/debug :navigate-replace view-id params)
(navigation/navigate-replace (name view-id) params)))
+(defn- all-screens-params [db view screen-params]
+ (cond-> db
+ (and (seq screen-params) (:screen screen-params) (:params screen-params))
+ (all-screens-params (:screen screen-params) (:params screen-params))
+
+ (seq screen-params)
+ (assoc-in [:navigation/screen-params view] screen-params)))
+
(fx/defn navigate-to-cofx
[{:keys [db]} go-to-view-id screen-params]
{:db
- (cond-> (assoc db :view-id go-to-view-id)
- ;; TODO: Inspect the need of screen-params
- (and (seq screen-params) (:screen screen-params) (:params screen-params))
- (assoc-in [:navigation/screen-params (:screen screen-params)] (:params screen-params))
- (seq screen-params)
- (assoc-in [:navigation/screen-params go-to-view-id] screen-params))
+ (-> (assoc db :view-id go-to-view-id)
+ (all-screens-params go-to-view-id screen-params))
::navigate-to [go-to-view-id screen-params]})
(fx/defn navigate-to
diff --git a/src/status_im/node/core.cljs b/src/status_im/node/core.cljs
index 7a957160b5..bb25df0e45 100644
--- a/src/status_im/node/core.cljs
+++ b/src/status_im/node/core.cljs
@@ -115,6 +115,7 @@
:always
(assoc :WalletConfig {:Enabled true}
+ :LocalNotificationsConfig {:Enabled true}
:BrowsersConfig {:Enabled true}
:PermissionsConfig {:Enabled true}
:MailserversConfig {:Enabled true}
diff --git a/src/status_im/notifications/android.cljs b/src/status_im/notifications/android.cljs
new file mode 100644
index 0000000000..dfda12327d
--- /dev/null
+++ b/src/status_im/notifications/android.cljs
@@ -0,0 +1,17 @@
+(ns status-im.notifications.android
+ (:require ["react-native" :as react-native]
+ [quo.platform :as platform]
+ [taoensso.timbre :as log]))
+
+(defn pn-android []
+ (when platform/android?
+ (.-PushNotification ^js (.-NativeModules react-native))))
+
+(defn present-local-notification [opts]
+ (.presentLocalNotification ^js (pn-android) (clj->js opts)))
+
+(defn create-channel [{:keys [channel-id channel-name]}]
+ (.createChannel ^js (pn-android)
+ #js {:channelId channel-id
+ :channelName channel-name}
+ #(log/info "Notifications create channel:" %)))
diff --git a/src/status_im/notifications/core.cljs b/src/status_im/notifications/core.cljs
index f2b67a8fed..8cad3f6d63 100644
--- a/src/status_im/notifications/core.cljs
+++ b/src/status_im/notifications/core.cljs
@@ -4,6 +4,7 @@
[status-im.utils.fx :as fx]
[status-im.multiaccounts.update.core :as multiaccounts.update]
["@react-native-community/push-notification-ios" :default pn-ios]
+ [status-im.notifications.android :as pn-android]
[status-im.native-module.core :as status]
[quo.platform :as platform]
[status-im.utils.config :as config]
@@ -64,7 +65,10 @@
::enable
(fn [_]
(if platform/android?
- (status/enable-notifications)
+ (do
+ (pn-android/create-channel {:channel-id "status-im-notifications"
+ :channel-name "Status push notifications"})
+ (status/enable-notifications))
(enable-ios-notifications))))
(re-frame/reg-fx
@@ -224,3 +228,37 @@
:on-success #(do
(log/info "[push-notifications] servers fetched" %)
(re-frame/dispatch [::servers-fetched %]))}]})
+
+;; Wallet transactions
+
+(fx/defn handle-preferences-load
+ {:events [::preferences-loaded]}
+ [{:keys [db]} preferences]
+ {:db (assoc db :push-notifications/preferences preferences)})
+
+(fx/defn load-notification-preferences
+ {:events [::load-notification-preferences]}
+ [cofx]
+ {::json-rpc/call [{:method "localnotifications_notificationPreferences"
+ :params []
+ :on-success #(re-frame/dispatch [::preferences-loaded %])}]})
+
+(defn preference= [x y]
+ (and (= (:service x) (:service y))
+ (= (:event x) (:event y))
+ (= (:identifier x) (:identifier y))))
+
+(defn- update-preference [all new]
+ (conj (filter (comp not (partial preference= new)) all) new))
+
+(fx/defn switch-transaction-notifications
+ {:events [::switch-transaction-notifications]}
+ [{:keys [db] :as cofx} enabled?]
+ {:db (update db :push-notifications/preferences update-preference {:enabled (not enabled?)
+ :service "wallet"
+ :event "transaction"
+ :identifier "all"})
+ ::json-rpc/call [{:method "localnotifications_switchWalletNotifications"
+ :params [(not enabled?)]
+ :on-success #(log/info "[push-notifications] switch-transaction-notifications successful" %)
+ :on-error #(log/error "[push-notifications] switch-transaction-notifications error" %)}]})
diff --git a/src/status_im/notifications/local.cljs b/src/status_im/notifications/local.cljs
new file mode 100644
index 0000000000..717cf3d70e
--- /dev/null
+++ b/src/status_im/notifications/local.cljs
@@ -0,0 +1,118 @@
+(ns status-im.notifications.local
+ (:require [taoensso.timbre :as log]
+ [clojure.string :as cstr]
+ [status-im.utils.fx :as fx]
+ [status-im.ethereum.decode :as decode]
+ ["@react-native-community/push-notification-ios" :default pn-ios]
+ [status-im.notifications.android :as pn-android]
+ [status-im.ethereum.tokens :as tokens]
+ [status-im.utils.utils :as utils]
+ [status-im.utils.types :as types]
+ [status-im.utils.money :as money]
+ [status-im.ethereum.core :as ethereum]
+ [status-im.i18n :as i18n]
+ [quo.platform :as platform]
+ [re-frame.core :as re-frame]
+ [status-im.ui.components.react :as react]
+ [cljs-bean.core :as bean]))
+
+(def default-erc20-token
+ {:symbol :ERC20
+ :decimals 18
+ :name "ERC20"})
+
+(def notification-event-ios "localNotification")
+(def notification-event-android "remoteNotificationReceived")
+
+(defn local-push-ios [{:keys [title message user-info]}]
+ (.presentLocalNotification pn-ios #js {:alertBody message
+ :alertTitle title
+ ;; NOTE: Use a special type to hide in Obj-C code other notifications
+ :userInfo (bean/->js (merge user-info
+ {:notificationType "local-notification"}))}))
+
+(defn local-push-android [{:keys [title message icon user-info]}]
+ (pn-android/present-local-notification (merge {:channelId "status-im-notifications"
+ :title title
+ :message message
+ :showBadge false}
+ (when user-info
+ {:userInfo (bean/->js user-info)})
+ (when icon
+ {:largeIconUrl (:uri (react/resolve-asset-source icon))}))))
+
+(defn handle-notification-press [{{deep-link :deepLink} :userInfo
+ interaction :userInteraction}]
+ (when (and deep-link
+ (or platform/ios?
+ (and platform/android? interaction)))
+ (re-frame/dispatch [:universal-links/handle-url deep-link])))
+
+(defn listen-notifications []
+ (if platform/ios?
+ (.addEventListener ^js pn-ios
+ notification-event-ios
+ (fn [notification]
+ (handle-notification-press {:userInfo (bean/bean (.getData ^js notification))})))
+ (.addListener ^js react/device-event-emitter
+ notification-event-android
+ (fn [^js data]
+ (when (and data (.-dataJSON data))
+ (handle-notification-press (types/json->clj (.-dataJSON data))))))))
+
+(defn create-notification [{{:keys [state from to fromAccount toAccount value erc20 contract network]}
+ :body
+ :as notification}]
+ (let [chain (ethereum/chain-id->chain-keyword network)
+ token (if erc20
+ (get-in tokens/all-tokens-normalized [(keyword chain)
+ (cstr/lower-case contract)]
+ default-erc20-token)
+ (tokens/native-currency (keyword chain)))
+ amount (money/wei->ether (decode/uint value))
+ to (or (:name toAccount) (utils/get-shortened-address to))
+ from (or (:name fromAccount) (utils/get-shortened-address from))
+ title (case state
+ "inbound" (i18n/label :t/push-inbound-transaction {:value amount
+ :currency (:symbol token)})
+ "outbound" (i18n/label :t/push-outbound-transaction {:value amount
+ :currency (:symbol token)})
+ "failed" (i18n/label :t/push-failed-transaction {:value amount
+ :currency (:symbol token)})
+ nil)
+ description (case state
+ "inbound" (i18n/label :t/push-inbound-transaction-body {:from from
+ :to to})
+ "outbound" (i18n/label :t/push-outbound-transaction-body {:from from
+ :to to})
+ "failed" (i18n/label :t/push-failed-transaction-body {:value amount
+ :currency (:symbol token)
+ :to to})
+ nil)]
+ {:title title
+ :icon (get-in token [:icon :source])
+ :user-info notification
+ :message description}))
+
+(re-frame/reg-fx
+ ::local-push-ios
+ (fn [evt]
+ (-> evt create-notification local-push-ios)))
+
+(fx/defn process
+ [_ evt]
+ (when platform/ios?
+ {::local-push-ios evt}))
+
+(defn handle []
+ (fn [^js message]
+ (let [evt (types/json->clj (.-event message))]
+ (js/Promise.
+ (fn [on-success on-error]
+ (try
+ (when (= "local-notifications" (:type evt))
+ (-> (:event evt) create-notification local-push-android))
+ (on-success)
+ (catch :default e
+ (log/warn "failed to handle background notification" e)
+ (on-error e))))))))
diff --git a/src/status_im/router/core.cljs b/src/status_im/router/core.cljs
index 7d9dc5f82d..c942beab3b 100644
--- a/src/status_im/router/core.cljs
+++ b/src/status_im/router/core.cljs
@@ -44,6 +44,7 @@
"browser/" browser-extractor
["p/" :chat-id] :private-chat
"g/" group-chat-extractor
+ ["wallet/" :account] :wallet-account
["u/" :user-id] :user
["user/" :user-id] :user
["referral/" :referrer] :referrals}
@@ -168,6 +169,10 @@
{:type :referrals
:referrer referrer})
+(defn match-wallet-account [{:keys [account]}]
+ {:type :wallet-account
+ :account (when account (string/lower-case account))})
+
(defn handle-uri [chain uri cb]
(let [{:keys [handler route-params query-params]} (match-uri uri)]
(log/info "[router] uri " uri " matched " handler " with " route-params)
@@ -196,6 +201,9 @@
(= handler :referrals)
(cb (match-referral route-params))
+ (= handler :wallet-account)
+ (cb (match-wallet-account route-params))
+
(ethereum/address? uri)
(cb (address->eip681 uri))
diff --git a/src/status_im/signals/core.cljs b/src/status_im/signals/core.cljs
index 74404daf92..069d18c696 100644
--- a/src/status_im/signals/core.cljs
+++ b/src/status_im/signals/core.cljs
@@ -6,6 +6,7 @@
[status-im.multiaccounts.model :as multiaccounts.model]
[status-im.transport.filters.core :as transport.filters]
[status-im.transport.message.core :as transport.message]
+ [status-im.notifications.local :as local-notifications]
[status-im.utils.fx :as fx]
[taoensso.timbre :as log]))
@@ -61,4 +62,5 @@
"whisper.filter.added" (transport.filters/handle-negotiated-filter cofx (js->clj event-js :keywordize-keys true))
"messages.new" (transport.message/process-response cofx event-js)
"wallet" (ethereum.subscriptions/new-wallet-event cofx (js->clj event-js :keywordize-keys true))
+ "local-notifications" (local-notifications/process cofx (js->clj event-js :keywordize-keys true))
(log/debug "Event " type " not handled"))))
diff --git a/src/status_im/subs.cljs b/src/status_im/subs.cljs
index 87ddd2299e..f6c71f1f42 100644
--- a/src/status_im/subs.cljs
+++ b/src/status_im/subs.cljs
@@ -43,7 +43,8 @@
status-im.ui.screens.keycard.settings.subs
status-im.ui.screens.keycard.pin.subs
status-im.ui.screens.keycard.setup.subs
- [status-im.chat.models.mentions :as mentions]))
+ [status-im.chat.models.mentions :as mentions]
+ [status-im.notifications.core :as notifications]))
;; TOP LEVEL ===========================================================================================================
@@ -209,6 +210,7 @@
;; push notifications
(reg-root-key-sub :push-notifications/servers :push-notifications/servers)
+(reg-root-key-sub :push-notifications/preferences :push-notifications/preferences)
;;GENERAL ==============================================================================================================
@@ -520,13 +522,16 @@
:<- [:multiaccount/accounts]
:<- [:get-screen-params :wallet-account]
(fn [[accounts acc]]
- (some #(when (= (:address %) (:address acc)) %) accounts)))
+ (some #(when (= (string/lower-case (:address %))
+ (string/lower-case (:address acc))) %) accounts)))
(re-frame/reg-sub
:account-by-address
:<- [:multiaccount/accounts]
(fn [accounts [_ address]]
- (some #(when (= (:address %) address) %) accounts)))
+ (when (and (string? address))
+ (some #(when (= (string/lower-case (:address %))
+ (string/lower-case address)) %) accounts))))
(re-frame/reg-sub
:multiple-multiaccounts?
@@ -2339,3 +2344,13 @@
:<- [:networks/manage]
(fn [manage]
(not-any? :error (vals manage))))
+
+;; NOTIFICATIONS
+
+(re-frame/reg-sub
+ :notifications/wallet-transactions
+ :<- [:push-notifications/preferences]
+ (fn [pref]
+ (first (filter #(notifications/preference= % {:service "wallet"
+ :event "transaction"
+ :identifier "all"}) pref))))
diff --git a/src/status_im/ui/components/react.cljs b/src/status_im/ui/components/react.cljs
index e2d0c58788..b2ff393854 100644
--- a/src/status_im/ui/components/react.cljs
+++ b/src/status_im/ui/components/react.cljs
@@ -32,6 +32,7 @@
(def image-class (reagent/adapt-react-class (.-Image react-native)))
(defn image-get-size [uri callback] (.getSize (.-Image react-native) uri callback))
+(defn resolve-asset-source [uri] (js->clj (.resolveAssetSource (.-Image react-native) uri) :keywordize-keys true))
(def linear-gradient (reagent/adapt-react-class LinearGradient))
diff --git a/src/status_im/ui/screens/notifications_settings/views.cljs b/src/status_im/ui/screens/notifications_settings/views.cljs
index 4f2041f39a..52d2c26b9a 100644
--- a/src/status_im/ui/screens/notifications_settings/views.cljs
+++ b/src/status_im/ui/screens/notifications_settings/views.cljs
@@ -4,6 +4,7 @@
[reagent.core :as reagent]
[status-im.i18n :as i18n]
[quo.core :as quo]
+ [quo.platform :as platform]
[quo.design-system.colors :as quo-colors]
[status-im.notifications.core :as notifications]
[status-im.ui.components.colors :as colors]
@@ -13,44 +14,79 @@
(defonce server (reagent/atom ""))
-(defn notifications-settings []
+(defn local-notifications []
+ [:<>
+ (let [{:keys [enabled]} @(re-frame/subscribe [:notifications/wallet-transactions])]
+ [quo/separator {:color (:ui-02 @quo-colors/theme)
+ :style {:margin-vertical 8}}]
+ [quo/list-header (i18n/label :t/local-notifications)]
+ [quo/list-item
+ {:size :small
+ :title (i18n/label :t/notifications-transactions)
+ :accessibility-label :notifications-button
+ :active enabled
+ :on-press #(re-frame/dispatch
+ [::notifications/switch-transaction-notifications enabled])
+ :accessory :switch}])])
+
+(defn notifications-settings-ios []
(let [{:keys [remote-push-notifications-enabled?
push-notifications-block-mentions?
push-notifications-from-contacts-only?]}
@(re-frame/subscribe [:multiaccount])]
- [react/view {:flex 1}
- [topbar/topbar {:title (i18n/label :t/notification-settings)}]
- [react/scroll-view {:style {:flex 1}
- :content-container-style {:padding-vertical 8}}
- [quo/list-item
- {:size :small
- :title (i18n/label :t/show-notifications)
- :accessibility-label :notifications-button
- :active remote-push-notifications-enabled?
- :on-press #(re-frame/dispatch [::notifications/switch (not remote-push-notifications-enabled?)])
- :accessory :switch}]
- [react/view {:height 1
- :background-color (:ui-02 @quo-colors/theme)
- :margin-vertical 8}]
- [quo/list-header (i18n/label :t/notifications-preferences)]
- [quo/list-item
- {:size :small
- :title (i18n/label :t/notifications-non-contacts)
- :accessibility-label :notifications-button
- :active (and remote-push-notifications-enabled?
- (not push-notifications-from-contacts-only?))
- :on-press #(re-frame/dispatch
- [::notifications/switch-non-contacts (not push-notifications-from-contacts-only?)])
- :accessory :switch}]
- [quo/list-item
- {:size :small
- :title (i18n/label :t/allow-mention-notifications)
- :accessibility-label :notifications-button
- :active (and remote-push-notifications-enabled?
- (not push-notifications-block-mentions?))
- :on-press #(re-frame/dispatch
- [::notifications/switch-block-mentions (not push-notifications-block-mentions?)])
- :accessory :switch}]]]))
+ [:<>
+ [quo/list-item
+ {:size :small
+ :title (i18n/label :t/show-notifications)
+ :accessibility-label :notifications-button
+ :active remote-push-notifications-enabled?
+ :on-press #(re-frame/dispatch [::notifications/switch (not remote-push-notifications-enabled?)])
+ :accessory :switch}]
+ [quo/separator {:color (:ui-02 @quo-colors/theme)
+ :style {:margin-vertical 8}}]
+ [quo/list-header (i18n/label :t/notifications-preferences)]
+ [quo/list-item
+ {:size :small
+ :title (i18n/label :t/notifications-non-contacts)
+ :accessibility-label :notifications-button
+ :active (and remote-push-notifications-enabled?
+ (not push-notifications-from-contacts-only?))
+ :on-press #(re-frame/dispatch
+ [::notifications/switch-non-contacts (not push-notifications-from-contacts-only?)])
+ :accessory :switch}]
+ [quo/list-item
+ {:size :small
+ :title (i18n/label :t/allow-mention-notifications)
+ :accessibility-label :notifications-button
+ :active (and remote-push-notifications-enabled?
+ (not push-notifications-block-mentions?))
+ :on-press #(re-frame/dispatch
+ [::notifications/switch-block-mentions (not push-notifications-block-mentions?)])
+ :accessory :switch}]
+ [local-notifications]]))
+
+(defn notifications-settings-android []
+ (let [{:keys [notifications-enabled?]}
+ @(re-frame/subscribe [:multiaccount])]
+ [:<>
+ [quo/list-item
+ {:icon :main-icons/notification
+ :title (i18n/label :t/notifications)
+ :accessibility-label :notifications-settings-button
+ :active notifications-enabled?
+ :on-press #(re-frame/dispatch
+ [::notifications/switch (not notifications-enabled?)])
+ :accessory :switch}]
+ [local-notifications]]))
+
+(defn notifications-settings []
+ [react/view {:flex 1}
+ [topbar/topbar {:title (i18n/label :t/notification-settings)}]
+ [react/scroll-view {:style {:flex 1}
+ :content-container-style {:padding-vertical 8}}
+ (if platform/ios?
+ [notifications-settings-ios]
+ [notifications-settings-android])]])
(defn notifications-advanced-settings []
(let [{:keys [remote-push-notifications-enabled?
diff --git a/src/status_im/ui/screens/profile/user/views.cljs b/src/status_im/ui/screens/profile/user/views.cljs
index 52dcc65300..981e8b49f1 100644
--- a/src/status_im/ui/screens/profile/user/views.cljs
+++ b/src/status_im/ui/screens/profile/user/views.cljs
@@ -3,7 +3,6 @@
[reagent.core :as reagent]
[status-im.i18n :as i18n]
[quo.core :as quo]
- [status-im.notifications.core :as notifications]
[status-im.ui.components.colors :as colors]
[status-im.multiaccounts.core :as multiaccounts]
[status-im.ui.components.common.common :as components.common]
@@ -98,7 +97,6 @@
(defn content []
(let [{:keys [preferred-name
mnemonic
- notifications-enabled?
keycard-pairing]}
@(re-frame/subscribe [:multiaccount])
chain @(re-frame/subscribe [:chain-keyword])
@@ -153,23 +151,12 @@
:accessibility-label :appearance-settings-button
:chevron true
:on-press #(re-frame/dispatch [:navigate-to :appearance])}]
- (if platform/ios?
- [quo/list-item
- {:icon :main-icons/notification
- :title (i18n/label :t/notifications)
- :accessibility-label :notifications-settings-button
- :chevron true
- :on-press #(re-frame/dispatch [:navigate-to :notifications])}]
- (when (and platform/android?
- config/local-notifications?)
- [quo/list-item
- {:icon :main-icons/notification
- :title (i18n/label :t/notifications)
- :accessibility-label :notifications-settings-button
- :active notifications-enabled?
- :on-press #(re-frame/dispatch
- [::notifications/switch (not notifications-enabled?)])
- :accessory :switch}]))
+ [quo/list-item
+ {:icon :main-icons/notification
+ :title (i18n/label :t/notifications)
+ :accessibility-label :notifications-settings-button
+ :chevron true
+ :on-press #(re-frame/dispatch [:navigate-to :notifications])}]
[quo/list-item
{:icon :main-icons/mobile
:title (i18n/label :t/sync-settings)
diff --git a/src/status_im/utils/universal_links/core.cljs b/src/status_im/utils/universal_links/core.cljs
index 0d556cbc84..5710b16ffd 100644
--- a/src/status_im/utils/universal_links/core.cljs
+++ b/src/status_im/utils/universal_links/core.cljs
@@ -1,5 +1,6 @@
(ns status-im.utils.universal-links.core
(:require [goog.string :as gstring]
+ [clojure.string :as string]
[re-frame.core :as re-frame]
[status-im.multiaccounts.model :as multiaccounts.model]
[status-im.chat.models :as chat]
@@ -84,6 +85,20 @@
;; TODO: Use only for testing
{::acquisition/check-referrer referrer})
+(defn existing-account? [{:keys [db]} address]
+ (when address
+ (some #(when (= (string/lower-case (:address %))
+ (string/lower-case address)) %)
+ (:multiaccount/accounts db))))
+
+(fx/defn handle-wallet-account [cofx {address :account}]
+ (when-let [account (existing-account? cofx address)]
+ (navigation/navigate-to-cofx cofx
+ :tabs
+ {:screen :wallet-stack
+ :params {:screen :wallet-account
+ :params account}})))
+
(defn handle-not-found [full-url]
(log/info "universal-links: no handler for " full-url))
@@ -98,13 +113,14 @@
{:events [::match-value]}
[cofx url {:keys [type] :as data}]
(case type
- :group-chat (handle-group-chat cofx data)
- :public-chat (handle-public-chat cofx data)
- :private-chat (handle-private-chat cofx data)
- :contact (handle-view-profile cofx data)
- :browser (handle-browse cofx data)
- :eip681 (handle-eip681 cofx data)
- :referrals (handle-referrer-url cofx data)
+ :group-chat (handle-group-chat cofx data)
+ :public-chat (handle-public-chat cofx data)
+ :private-chat (handle-private-chat cofx data)
+ :contact (handle-view-profile cofx data)
+ :browser (handle-browse cofx data)
+ :eip681 (handle-eip681 cofx data)
+ :referrals (handle-referrer-url cofx data)
+ :wallet-account (handle-wallet-account cofx data)
(handle-not-found url)))
(fx/defn route-url
diff --git a/status-go-version.json b/status-go-version.json
index 5e667444d9..6767ddc61f 100644
--- a/status-go-version.json
+++ b/status-go-version.json
@@ -2,7 +2,7 @@
"_comment": "DO NOT EDIT THIS FILE BY HAND. USE 'scripts/update-status-go.sh ' instead",
"owner": "status-im",
"repo": "status-go",
- "version": "v0.62.14",
- "commit-sha1": "b3880027710ee7a28bbdeacbe099412627485d62",
- "src-sha256": "0g3c1rb0hnqr6wk09n4zk9985qs9m7v44f03vfdsrbzvzx93wgkd"
+ "version": "v0.62.16",
+ "commit-sha1": "d04e54e54e8ea85c9bd35aff5482e064f5ee76b0",
+ "src-sha256": "11ry2ncpf5i177c641nvs27b96niwbfwqc79vn3dqkdawr7y0aiy"
}
diff --git a/test/appium/tests/atomic/chats/test_one_to_one.py b/test/appium/tests/atomic/chats/test_one_to_one.py
index d69f50361e..15b0d04286 100644
--- a/test/appium/tests/atomic/chats/test_one_to_one.py
+++ b/test/appium/tests/atomic/chats/test_one_to_one.py
@@ -47,6 +47,7 @@ class TestMessagesOneToOneChatMultiple(MultipleDeviceTestCase):
default_username_1 = profile_1.default_username_text.text
profile_1.settings_button.click()
profile_1.profile_notifications_button.click()
+ profile_1.profile_notifications_toggle_button.click()
device_1_home = profile_1.get_back_to_home_view()
device_2_public_key = device_2_home.get_public_key_and_username()
diff --git a/test/appium/views/profile_view.py b/test/appium/views/profile_view.py
index 88f89bb79f..722f2f3e1b 100644
--- a/test/appium/views/profile_view.py
+++ b/test/appium/views/profile_view.py
@@ -467,6 +467,12 @@ class PrifileNotificationsButton(BaseButton):
self.locator = self.Locator.accessibility_id("notifications-settings-button")
+class PrifileNotificationsToggleButton(BaseButton):
+ def __init__(self, driver):
+ super().__init__(driver)
+ self.locator = self.Locator.xpath_selector("//*[@content-desc='notifications-settings-button']")
+
+
class RemovePictureButton(BaseButton):
def __init__(self, driver):
super().__init__(driver)
@@ -645,6 +651,7 @@ class ProfileView(BaseView):
self.confirm_edit_button = ConfirmEditButton(self.driver)
self.cross_icon = CrossIcon(self.driver)
self.profile_notifications_button = PrifileNotificationsButton(self.driver)
+ self.profile_notifications_toggle_button = PrifileNotificationsToggleButton(self.driver)
self.advanced_button = AdvancedButton(self.driver)
self.log_level_setting = LogLevelSetting(self.driver)
self.debug_mode_toggle = DebugModeToggle(self.driver)
diff --git a/translations/en.json b/translations/en.json
index ed8a1019e3..a1e68dd27d 100644
--- a/translations/en.json
+++ b/translations/en.json
@@ -816,10 +816,17 @@
"notifications-preferences": "Notification preferences",
"notifications-switch": "Show notifications",
"notifications-non-contacts": "Notifications from non-contacts",
- "send-push-notifications": "Send push notifications",
+ "notifications-transactions": "Wallet transactions",
+ "local-notifications": "Local notifications",
"send-push-notifications-description": "When disabled, the person receiving your messages won't be notified of their arrival",
"push-notifications-server-enabled": "Server enabled",
"push-notifications-servers": "Push notification servers",
+ "push-inbound-transaction": "You received {{value}} {{currency}}",
+ "push-outbound-transaction": "You sent {{value}} {{currency}}",
+ "push-failed-transaction": "Your transaction failed",
+ "push-inbound-transaction-body": "From {{from}} to {{to}}",
+ "push-outbound-transaction-body": "From {{from}} to {{to}}",
+ "push-failed-transaction-body": "{{value}} {{currency}} to {{to}}",
"allow-mention-notifications": "Show @ mentions",
"server": "Server",
"specify-server-public-key": "Enter server public key",