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:
parent
10dcee3e05
commit
050f20cfae
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
(def react-native
|
||||
(clj->js {:NativeModules {:RNGestureHandlerModule {:Direction (fn [])}
|
||||
:PushNotifications {}
|
||||
:ReanimatedModule {:configureProps (fn [])}}
|
||||
|
||||
:View {}
|
||||
|
|
|
@ -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!))
|
||||
|
|
|
@ -109,6 +109,8 @@
|
|||
"wakuext_enablePushNotificationsBlockMentions" {}
|
||||
"wakuext_disablePushNotificationsBlockMentions" {}
|
||||
"status_chats" {}
|
||||
"localnotifications_switchWalletNotifications" {}
|
||||
"localnotifications_notificationPreferences" {}
|
||||
"wallet_getTransfers" {}
|
||||
"wallet_getTokensBalances" {}
|
||||
"wallet_getBlocksByAddress" {}
|
||||
|
|
|
@ -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)))
|
||||
|
||||
|
|
|
@ -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)))))
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -115,6 +115,7 @@
|
|||
|
||||
:always
|
||||
(assoc :WalletConfig {:Enabled true}
|
||||
:LocalNotificationsConfig {:Enabled true}
|
||||
:BrowsersConfig {:Enabled true}
|
||||
:PermissionsConfig {:Enabled true}
|
||||
:MailserversConfig {:Enabled true}
|
||||
|
|
|
@ -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:" %)))
|
|
@ -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" %)}]})
|
||||
|
|
|
@ -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))))))))
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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"))))
|
||||
|
|
|
@ -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))))
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue