-
-
Notifications
You must be signed in to change notification settings - Fork 69
feat: add push notifications for web and android #802
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Linhead
wants to merge
33
commits into
dev
Choose a base branch
from
feat/add-push-notifications
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
f92a5cb
feat: add push notifications for web and android
Linhead 76c493e
fix: code-review fixes
Linhead e50ccc5
fix: code review fix
Linhead 47b290e
feat: unify PWA and Firebase service workers
Linhead 6824162
fix: code-review fixes
Linhead 6dbd81f
update: update capacitor
Linhead c07a706
fix: fix crash on android
Linhead 1249aa8
refactor/feat/fix: fix bugs, refactoring, creting separate firebase S…
Linhead ce1ab78
fix: add loader when select notifications type + change default messa…
Linhead a281a67
refactor: add usePushNotificationSetup + add handling user cases with…
Linhead 6c8fdca
refactor: eliminate push notification privateKey duplication using pl…
Linhead a4aa11f
refactor: clean up push notifications code and fix redundant checks
Linhead 956329a
feat: add push notifications for android app
Linhead 221649a
fix: remove useless deep links for android
Linhead d8c25c4
fix: add safe closing broadcast channel in case of error during the p…
Linhead 030c3dd
fix: code-review fixes
Linhead 7f754dd
fix: code-revies fixes
Linhead 3b5ae72
fix: code-review fixes
Linhead ccf4f8c
fix: ensure public key is loaded before sending push notifications
Linhead ac4ceca
feat: add block notification clicks for different user accounts for w…
Linhead 181201b
fix: prevent Firebase service notifications from leaking through
Linhead 7850419
fix: remove unnecessary firebase-analytics dependency
Linhead 20eccc1
fix: add event-driven settings initialization in service worker
Linhead 1a04336
fix: use full ADAMANT address in notifications
Linhead 1c3d698
Update public/firebase/firebase-messaging-sw.js
Linhead 4a26ee9
Update public/firebase/firebase-messaging-sw.js
Linhead 1dbe881
feat: implement scroll to message on notification click + refactoring
Linhead 2d82222
feat: add secure private key storage for Android push notifications
Linhead dfc8aee
fix: disable unsupported Background Fetch option on Android
Linhead d67f29b
chore: store ANS public key as constant to avoid redundant API calls
Linhead 997cb58
refactor: replace timeout with Promise-based Service Worker registrat…
Linhead fd99e09
fix: fix bug with enabled background fetch item in notifications list…
Linhead 1817080
fix: replace setTimeout with navigator.serviceWorker.ready for push n…
Linhead File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| { | ||
| "project_info": { | ||
| "project_number": "987518845753", | ||
| "firebase_url": "https://adamant-messenger.firebaseio.com", | ||
| "project_id": "adamant-messenger", | ||
| "storage_bucket": "adamant-messenger.appspot.com" | ||
| }, | ||
| "client": [ | ||
| { | ||
| "client_info": { | ||
| "mobilesdk_app_id": "1:987518845753:android:6585b11ca36bac4c251ee8", | ||
| "android_client_info": { | ||
| "package_name": "im.adamant.adamantmessengerpwa" | ||
| } | ||
| }, | ||
| "api_key": [ | ||
| { | ||
| "current_key": "AIzaSyDgtB_hqwL1SS_YMYepRMmXYhmc7154wmU" | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "configuration_version": "1" | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
308 changes: 308 additions & 0 deletions
308
...oid/app/src/main/java/im/adamant/adamantmessengerpwa/AdamantFirebaseMessagingService.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,308 @@ | ||
| package im.adamant.adamantmessengerpwa; | ||
|
|
||
| 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.os.Build; | ||
| import android.util.Log; | ||
| import androidx.core.app.NotificationCompat; | ||
| import com.google.firebase.messaging.FirebaseMessagingService; | ||
| import com.google.firebase.messaging.RemoteMessage; | ||
| import java.util.HashSet; | ||
| import android.os.Handler; | ||
| import android.os.Looper; | ||
| import org.json.JSONObject; | ||
|
|
||
| public class AdamantFirebaseMessagingService extends FirebaseMessagingService { | ||
|
|
||
| private static final String TAG = "AdamantFirebaseMsg"; | ||
| private static final String CHANNEL_ID = "adamant_notifications"; | ||
| private static final long CLEANUP_INTERVAL = 2 * 60 * 1000; | ||
|
|
||
| private static final HashSet<String> processedEvents = new HashSet<>(); | ||
| private static Handler cleanupHandler = new Handler(Looper.getMainLooper()); | ||
|
|
||
| static { | ||
| cleanupHandler.postDelayed(new Runnable() { | ||
| @Override | ||
| public void run() { | ||
| processedEvents.clear(); | ||
| cleanupHandler.postDelayed(this, CLEANUP_INTERVAL); | ||
| } | ||
| }, CLEANUP_INTERVAL); | ||
| } | ||
|
|
||
| @Override | ||
| public void onCreate() { | ||
| super.onCreate(); | ||
| createNotificationChannel(); | ||
| } | ||
|
|
||
| @Override | ||
| public void handleIntent(Intent intent) { | ||
| RemoteMessage remoteMessage = null; | ||
| try { | ||
| if (intent.getExtras() != null) { | ||
| remoteMessage = new RemoteMessage(intent.getExtras()); | ||
| } | ||
| } catch (Exception e) { | ||
| Log.e(TAG, "Error creating RemoteMessage", e); | ||
| } | ||
|
|
||
| if (remoteMessage == null) { | ||
| super.handleIntent(intent); | ||
| return; | ||
| } | ||
|
|
||
| if (areNotificationsDisabled()) { | ||
| return; | ||
| } | ||
|
|
||
| String txnData = remoteMessage.getData().get("txn"); | ||
| if (txnData != null) { | ||
| String transactionId = extractTransactionId(txnData); | ||
| if (transactionId != null) { | ||
| if (processedEvents.contains(transactionId)) { | ||
| return; | ||
| } | ||
| processedEvents.add(transactionId); | ||
|
|
||
| // Also check if this is a signal message that should be hidden | ||
| String body = formatNotificationText(txnData); | ||
| if (body == null) { | ||
| // Signal message - don't show notification but still mark as processed | ||
| return; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (isAppInForeground()) { | ||
| super.handleIntent(intent); | ||
| } else { | ||
| showBackgroundNotification(remoteMessage); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void onMessageReceived(RemoteMessage remoteMessage) { | ||
| super.onMessageReceived(remoteMessage); | ||
| } | ||
|
|
||
| private void showBackgroundNotification(RemoteMessage remoteMessage) { | ||
| try { | ||
| String txnData = remoteMessage.getData().get("txn"); | ||
| if (txnData == null) { | ||
| return; | ||
| } | ||
|
|
||
| String transactionId = extractTransactionId(txnData); | ||
| String senderId = extractSenderId(txnData); | ||
| if (transactionId == null || senderId == null) { | ||
| return; | ||
| } | ||
|
|
||
| String title = senderId; | ||
| String body = formatNotificationText(txnData); | ||
|
|
||
| Intent clickIntent = new Intent(this, MainActivity.class); | ||
| clickIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); | ||
| clickIntent.putExtra("openChat", true); | ||
| clickIntent.putExtra("senderId", senderId); | ||
| clickIntent.putExtra("transactionId", transactionId); | ||
|
|
||
| PendingIntent pendingIntent = PendingIntent.getActivity( | ||
| this, | ||
| transactionId.hashCode(), | ||
| clickIntent, | ||
| PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE | ||
| ); | ||
|
|
||
| NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID) | ||
| .setSmallIcon(android.R.drawable.ic_dialog_info) | ||
| .setContentTitle(title) | ||
| .setContentText(body) | ||
| .setAutoCancel(true) | ||
| .setPriority(NotificationCompat.PRIORITY_DEFAULT) | ||
| .setContentIntent(pendingIntent); | ||
|
|
||
| NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); | ||
| int notificationId = Math.abs(transactionId.hashCode()); | ||
| notificationManager.notify(notificationId, builder.build()); | ||
|
|
||
| MainActivity.saveNotificationId(this, senderId, notificationId); | ||
|
|
||
| } catch (Exception e) { | ||
| Log.e(TAG, "Error showing notification", e); | ||
| } | ||
| } | ||
|
|
||
| private String formatNotificationText(String txnData) { | ||
| try { | ||
| JSONObject transaction = new JSONObject(txnData); | ||
|
|
||
| int transactionType = transaction.optInt("type", -1); | ||
| long transactionAmount = transaction.optLong("amount", 0); | ||
|
|
||
| // Type 0: Pure ADM Transfer (without comments) | ||
| if (transactionType == 0) { | ||
| return formatADMTransfer(transactionAmount); | ||
| } | ||
|
|
||
| // Type 8: Chat Message (can include ADM transfer with comment) | ||
| if (transactionType == 8) { | ||
| // If amount > 0, it's ADM transfer with comment | ||
| if (transactionAmount > 0) { | ||
| return formatADMTransferWithComment(transactionAmount); | ||
| } | ||
|
|
||
| // Regular chat message - check asset.chat.type | ||
| return formatChatMessage(transaction); | ||
| } | ||
|
|
||
| return "New transaction"; | ||
| } catch (Exception e) { | ||
| Log.e(TAG, "Error parsing notification", e); | ||
| return "New message"; | ||
| } | ||
| } | ||
|
|
||
| private String formatChatMessage(JSONObject transaction) { | ||
| try { | ||
| if (!transaction.has("asset")) { | ||
| return "New message"; | ||
| } | ||
|
|
||
| JSONObject asset = transaction.getJSONObject("asset"); | ||
| if (!asset.has("chat")) { | ||
| return "New message"; | ||
| } | ||
|
|
||
| JSONObject chat = asset.getJSONObject("chat"); | ||
| int chatType = chat.optInt("type", 1); | ||
|
|
||
| switch (chatType) { | ||
| case 1: | ||
| // Basic Encrypted Message | ||
| return "New message"; | ||
| case 2: | ||
| // Rich Content Message (crypto transfers, etc.) | ||
| return "sent you crypto"; | ||
| case 3: | ||
| // Signal Message (should be hidden per documentation) | ||
| return null; // Don't show notification for signal messages | ||
| default: | ||
| return "New message"; | ||
| } | ||
| } catch (Exception e) { | ||
| return "New message"; | ||
| } | ||
| } | ||
|
|
||
| private String formatADMTransfer(long amount) { | ||
| if (amount == 0) { | ||
| return "sent you ADM"; | ||
| } | ||
| double admAmount = amount / 100000000.0; | ||
|
|
||
| java.math.BigDecimal bd = java.math.BigDecimal.valueOf(admAmount); | ||
| return String.format("sent you %s ADM", bd.stripTrailingZeros().toPlainString()); | ||
| } | ||
|
|
||
| private String formatADMTransferWithComment(long amount) { | ||
| if (amount == 0) { | ||
| return "sent you ADM with message"; | ||
| } | ||
| double admAmount = amount / 100000000.0; | ||
|
|
||
| java.math.BigDecimal bd = java.math.BigDecimal.valueOf(admAmount); | ||
| return String.format("sent you %s ADM with message", bd.stripTrailingZeros().toPlainString()); | ||
| } | ||
|
|
||
| private boolean areNotificationsDisabled() { | ||
| try { | ||
| SharedPreferences prefs = getSharedPreferences("CapacitorStorage", Context.MODE_PRIVATE); | ||
| String notificationTypeStr = prefs.getString("allowNotificationType", "2"); | ||
| return "0".equals(notificationTypeStr); | ||
| } catch (Exception e) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| private boolean isAppInForeground() { | ||
| try { | ||
| android.app.ActivityManager activityManager = (android.app.ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); | ||
| java.util.List<android.app.ActivityManager.RunningAppProcessInfo> appProcesses = activityManager.getRunningAppProcesses(); | ||
|
|
||
| if (appProcesses == null) { | ||
| return false; | ||
| } | ||
|
|
||
| String packageName = getPackageName(); | ||
| for (android.app.ActivityManager.RunningAppProcessInfo appProcess : appProcesses) { | ||
| if (appProcess.processName.equals(packageName)) { | ||
| return appProcess.importance == android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; | ||
| } | ||
| } | ||
| return false; | ||
| } catch (Exception e) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| private String extractTransactionId(String txnData) { | ||
| try { | ||
| JSONObject transaction = new JSONObject(txnData); | ||
| return transaction.optString("id", null); | ||
| } catch (Exception e) { | ||
| // Fallback to string parsing | ||
| return extractFieldFromJson(txnData, "\"id\":\"", 6); | ||
| } | ||
| } | ||
|
|
||
| private String extractSenderId(String txnData) { | ||
| try { | ||
| JSONObject transaction = new JSONObject(txnData); | ||
| return transaction.optString("senderId", null); | ||
| } catch (Exception e) { | ||
| // Fallback to string parsing | ||
| return extractFieldFromJson(txnData, "\"senderId\":\"", 12); | ||
| } | ||
| } | ||
|
|
||
| private String extractFieldFromJson(String jsonData, String fieldPattern, int patternLength) { | ||
| try { | ||
| int startIndex = jsonData.indexOf(fieldPattern); | ||
| if (startIndex == -1) { | ||
| return null; | ||
| } | ||
|
|
||
| startIndex += patternLength; | ||
| int endIndex = jsonData.indexOf("\"", startIndex); | ||
| return endIndex == -1 ? null : jsonData.substring(startIndex, endIndex); | ||
| } catch (Exception e) { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| private void createNotificationChannel() { | ||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||
| NotificationChannel channel = new NotificationChannel( | ||
| CHANNEL_ID, | ||
| "ADAMANT Notifications", | ||
| NotificationManager.IMPORTANCE_DEFAULT | ||
| ); | ||
| channel.setDescription("Notifications for ADAMANT Messenger"); | ||
|
|
||
| NotificationManager notificationManager = getSystemService(NotificationManager.class); | ||
| notificationManager.createNotificationChannel(channel); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void onNewToken(String token) { | ||
| super.onNewToken(token); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it correct to store plain API key in the repository?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This API key is already used in two other places in the repository: src/lib/firebase-config.ts and public/firebase/firebase-messaging-sw.js. So there's no point in hiding it here while it's already exposed in the other files.
According to Firebase documentation (https://firebase.google.com/docs/projects/api-keys), API keys for Firebase services are safe to include in code since they're used only for project identification, not authorization. All Firebase-provisioned API keys have API restrictions applied by default, and we can set additional restrictions through Google Cloud Console if needed.
I've analyzed potential security risks and concluded that exposure is minimal since ADAMANT only uses Firebase for FCM (no data storage), and the main theoretical risk is quota abuse (but FCM has unlimited free tier).