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 <feross95@gmail.com>
This commit is contained in:
Gheorghe Pinzaru 2020-09-25 15:35:10 +03:00
parent 10dcee3e05
commit 050f20cfae
No known key found for this signature in database
GPG Key ID: C9A094959935A952
36 changed files with 1461 additions and 80 deletions

View File

@ -75,6 +75,7 @@
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>
<service android:name="im.status.ethereum.module.ForegroundService"></service>
<service android:name="im.status.ethereum.module.LocalNotificationsService" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"

View File

@ -18,6 +18,7 @@ import java.util.List;
import im.status.ethereum.keycard.RNStatusKeycardPackage;
import im.status.ethereum.module.StatusPackage;
import im.status.ethereum.pushnotifications.PushNotificationPackage;
public class MainApplication extends MultiDexApplication implements ReactApplication {
@ -30,10 +31,12 @@ public class MainApplication extends MultiDexApplication implements ReactApplica
@Override
protected List<ReactPackage> getPackages() {
StatusPackage statusPackage = new StatusPackage(RootUtil.isDeviceRooted());
List<ReactPackage> packages = new PackageList(this).getPackages();
packages.add(statusPackage);
packages.add(new ReactNativeDialogsPackage());
packages.add(new RNStatusKeycardPackage());
packages.add(new PushNotificationPackage());
return packages;
}

View File

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

View File

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

View File

@ -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;
}
}

View File

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

View File

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

View File

@ -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();
}
}
}
});
}
}
}

View File

@ -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<RunningAppProcessInfo> 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;
}
}

View File

@ -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<String> 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);
}
}
}

View File

@ -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<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Collections.<NativeModule>singletonList(new PushNotification(reactContext));
}
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}

View File

@ -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<CloseableReference<CloseableImage>> 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);
}
}
}
}

View File

@ -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];

View File

@ -18,6 +18,7 @@
(def react-native
(clj->js {:NativeModules {:RNGestureHandlerModule {:Direction (fn [])}
:PushNotifications {}
:ReanimatedModule {:configureProps (fn [])}}
:View {}

View File

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

View File

@ -109,6 +109,8 @@
"wakuext_enablePushNotificationsBlockMentions" {}
"wakuext_disablePushNotificationsBlockMentions" {}
"status_chats" {}
"localnotifications_switchWalletNotifications" {}
"localnotifications_notificationPreferences" {}
"wallet_getTransfers" {}
"wallet_getTokensBalances" {}
"wallet_getBlocksByAddress" {}

View File

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

View File

@ -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
@ -59,7 +64,9 @@
(fx/merge
cofx
{:db (assoc db :multiaccount/accounts
(rpc->accounts 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?)
@ -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)))))

View File

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

View File

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

View File

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

View File

@ -115,6 +115,7 @@
:always
(assoc :WalletConfig {:Enabled true}
:LocalNotificationsConfig {:Enabled true}
:BrowsersConfig {:Enabled true}
:PermissionsConfig {:Enabled true}
:MailserversConfig {:Enabled true}

View File

@ -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:" %)))

View File

@ -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" %)}]})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,15 +14,27 @@
(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)
@ -29,9 +42,8 @@
: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/separator {:color (:ui-02 @quo-colors/theme)
:style {:margin-vertical 8}}]
[quo/list-header (i18n/label :t/notifications-preferences)]
[quo/list-item
{:size :small
@ -50,7 +62,31 @@
(not push-notifications-block-mentions?))
:on-press #(re-frame/dispatch
[::notifications/switch-block-mentions (not push-notifications-block-mentions?)])
:accessory :switch}]]]))
: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?

View File

@ -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/mobile
:title (i18n/label :t/sync-settings)

View File

@ -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))
@ -105,6 +120,7 @@
: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

View File

@ -2,7 +2,7 @@
"_comment": "DO NOT EDIT THIS FILE BY HAND. USE 'scripts/update-status-go.sh <tag>' 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"
}

View File

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

View File

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

View File

@ -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",