From 050f20cfae8443281c26197292dc9fafaf1d0a0e Mon Sep 17 00:00:00 2001 From: Gheorghe Pinzaru Date: Fri, 25 Sep 2020 15:35:10 +0300 Subject: [PATCH] Add local notifications for transactions Pods Add headless js service Handle Local Notifications react CopyPaste driven implementation of java notification pn demo Show iOs push in foreground Show icon in notification Enable notifications on login Get chain from status-go Add UI for switching notifications go go! Fixup Handle notification onPress Android UI Handle press iOs Handle android press and validate go update Fix route params in universal link handler Set show badge explicitly to false Fix e2e bump status go Signed-off-by: Gheorghe Pinzaru --- android/app/src/main/AndroidManifest.xml | 1 + .../im/status/ethereum/MainApplication.java | 3 + ios/Podfile.lock | 6 + ios/StatusIm/AppDelegate.m | 13 + .../module/LocalNotificationsService.java | 20 + .../status/ethereum/module/StatusModule.java | 25 + .../pushnotifications/PushNotification.java | 110 ++++ .../PushNotificationActions.java | 90 +++ .../PushNotificationHelper.java | 549 ++++++++++++++++++ .../PushNotificationJsDelivery.java | 86 +++ .../PushNotificationPackage.java | 27 + .../PushNotificationPicturesAggregator.java | 136 +++++ .../ios/RCTStatus/RCTStatus.m | 16 + src/mocks/js_dependencies.cljs | 1 + src/status_im/core.cljs | 4 + src/status_im/ethereum/json_rpc.cljs | 2 + src/status_im/ethereum/tokens.cljs | 14 +- src/status_im/multiaccounts/login/core.cljs | 18 +- src/status_im/multiaccounts/logout/core.cljs | 6 + src/status_im/native_module/core.cljs | 8 + src/status_im/navigation.cljs | 16 +- src/status_im/node/core.cljs | 1 + src/status_im/notifications/android.cljs | 17 + src/status_im/notifications/core.cljs | 40 +- src/status_im/notifications/local.cljs | 118 ++++ src/status_im/router/core.cljs | 8 + src/status_im/signals/core.cljs | 2 + src/status_im/subs.cljs | 21 +- src/status_im/ui/components/react.cljs | 1 + .../screens/notifications_settings/views.cljs | 104 ++-- .../ui/screens/profile/user/views.cljs | 25 +- src/status_im/utils/universal_links/core.cljs | 30 +- status-go-version.json | 6 +- .../tests/atomic/chats/test_one_to_one.py | 1 + test/appium/views/profile_view.py | 7 + translations/en.json | 9 +- 36 files changed, 1461 insertions(+), 80 deletions(-) create mode 100644 modules/react-native-status/android/src/main/java/im/status/ethereum/module/LocalNotificationsService.java create mode 100644 modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotification.java create mode 100644 modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationActions.java create mode 100644 modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationHelper.java create mode 100644 modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationJsDelivery.java create mode 100644 modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationPackage.java create mode 100644 modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationPicturesAggregator.java create mode 100644 src/status_im/notifications/android.cljs create mode 100644 src/status_im/notifications/local.cljs 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",