Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1316495
Basic implementation (refs #28)
Choochmeque Dec 13, 2025
3ca152e
Merge branch 'main' into notification-clicked-event-android
Choochmeque Dec 13, 2025
0f936ba
Merge branch 'main' into notification-clicked-event-android
Choochmeque Dec 19, 2025
a28e195
refactor(android): extract getActive logic to Notification companion …
Choochmeque Dec 19, 2025
f60325d
Add separate cancelAll command and refactor notification parsing
Choochmeque Dec 19, 2025
c183689
Fixed formatting
Choochmeque Dec 19, 2025
b22ec14
Add some logs
Choochmeque Dec 19, 2025
fa5bd13
Fix notification storage and add some logging
Choochmeque Dec 20, 2025
e151b42
Fix notification storage, action buttons, and add logging
Choochmeque Dec 20, 2025
32552e5
Fix cancelled recurring notifications still firing on Android
Choochmeque Dec 20, 2025
3af5796
Add debug logging for push notification clicks and configure demo app…
Choochmeque Dec 20, 2025
ffc0ae5
Add google-services.json to .gitignore
Choochmeque Dec 20, 2025
39c7709
Add desktop listener support for plugin events
Choochmeque Dec 22, 2025
cc239c0
Added native macos support
Choochmeque Dec 23, 2025
42e7c2b
Refactoring
Choochmeque Dec 23, 2025
acf3b72
Refactoring
Choochmeque Dec 23, 2025
2347a81
Merge branch 'main' into notification-macos
Choochmeque Dec 23, 2025
314de24
Unit tests (macos)
Choochmeque Dec 23, 2025
16e04f0
Cleanup
Choochmeque Dec 23, 2025
9b54f3f
Fixed ts tests
Choochmeque Dec 23, 2025
10184c3
Fixed unit tests (ios)
Choochmeque Dec 23, 2025
b01acb3
Cleanup
Choochmeque Dec 24, 2025
7a29c12
Removed hardcoded rpath on macos (set MACOSX_DEPLOYMENT_TARGET = "13.0")
Choochmeque Dec 24, 2025
aa1fa59
Fixed unit tests (android)
Choochmeque Dec 24, 2025
a379cdc
Cleanup
Choochmeque Dec 24, 2025
1b0b287
Fixed unit tests (android)
Choochmeque Dec 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[env]
MACOSX_DEPLOYMENT_TARGET = "13.0"
54 changes: 54 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,57 @@ jobs:
with:
name: ios-coverage
path: ios/coverage.json

test-macos:
name: macOS Unit Tests
runs-on: macos-latest

steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin

- name: Build macOS bindings
run: cargo build --target aarch64-apple-darwin

- name: Run macOS unit tests
run: |
cd macos
xcodebuild test \
-scheme tauri-plugin-notifications \
-destination 'platform=macOS' \
-enableCodeCoverage YES \
-resultBundlePath TestResults.xcresult

- name: Generate coverage report
run: |
cd macos
xcrun xccov view --report --json TestResults.xcresult > coverage.json

- name: Upload coverage to Codecov
if: ${{ github.actor != 'dependabot[bot]' }}
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
files: ./macos/coverage.json
flags: macos
name: macos-coverage

- name: Upload test results
uses: actions/upload-artifact@v6
if: always()
with:
name: macos-test-results
path: macos/TestResults.xcresult

- name: Upload coverage reports
uses: actions/upload-artifact@v6
if: always()
with:
name: macos-coverage
path: macos/coverage.json
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ node_modules/
dist-js
dist
coverage

# Firebase config (users should add their own)
google-services.json
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ node_modules
schema.json
target
**/gen/*
**/.svelte-kit/
11 changes: 9 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ license = "MIT"
repository = "https://github.com/Choochmeque/tauri-plugin-notifications"

[features]
default = []
default = ["notify-rust"]
push-notifications = []
notify-rust = ["dep:notify-rust"]

[dependencies]
tauri = { version = "2.8.2" }
Expand All @@ -28,12 +29,18 @@ url = { version = "2", features = ["serde"] }
[target.'cfg(target_os = "ios")'.dependencies]
tauri = { version = "2.8.2", features = ["wry"] }

[target.'cfg(target_os = "macos")'.dependencies]
swift-bridge = { version = "0.1", features = ["async"], git = "https://github.com/chinedufn/swift-bridge", rev = "82b0885" }

[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
notify-rust = "4.11"
notify-rust = { version = "4.11", optional = true }

[build-dependencies]
tauri-plugin = { version = "2.4.0", features = ["build"] }

[target.'cfg(target_os = "macos")'.build-dependencies]
swift-bridge-build = { version = "0.1", git = "https://github.com/chinedufn/swift-bridge", rev = "82b0885" }

[dev-dependencies]
color-backtrace = "0.7"
ctor = "0.6"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,13 +168,13 @@ class NotificationStorageInstrumentedTest {

val retrieved = storage.getActionGroup("message-actions")

// Note: There's a bug in NotificationStorage.writeActionGroup where it uses
// type.id instead of loop index, causing actions to overwrite each other.
// This test verifies the actual (buggy) behavior.
assertEquals(2, retrieved.size)
// Actions will be empty because write uses wrong key
assertTrue(retrieved[0]?.id.isNullOrEmpty())
assertTrue(retrieved[1]?.id.isNullOrEmpty())
assertEquals("reply", retrieved[0]?.id)
assertEquals("Reply", retrieved[0]?.title)
assertEquals(true, retrieved[0]?.input)
assertEquals("dismiss", retrieved[1]?.id)
assertEquals("Dismiss", retrieved[1]?.title)
assertEquals(false, retrieved[1]?.input)
}

@Test
Expand Down Expand Up @@ -260,13 +260,17 @@ class NotificationStorageInstrumentedTest {

val retrieved1 = storage.getActionGroup("group1")
assertEquals(1, retrieved1.size)
// Bug in writeActionGroup causes empty action ids
assertTrue(retrieved1[0]?.id.isNullOrEmpty())
assertEquals("action1", retrieved1[0]?.id)
assertEquals("Action 1", retrieved1[0]?.title)
assertEquals(false, retrieved1[0]?.input)

val retrieved2 = storage.getActionGroup("group2")
assertEquals(2, retrieved2.size)
// Bug in writeActionGroup causes empty action ids
assertTrue(retrieved2[0]?.id.isNullOrEmpty())
assertTrue(retrieved2[1]?.id.isNullOrEmpty())
assertEquals("action2", retrieved2[0]?.id)
assertEquals("Action 2", retrieved2[0]?.title)
assertEquals(true, retrieved2[0]?.input)
assertEquals("action3", retrieved2[1]?.id)
assertEquals("Action 3", retrieved2[1]?.title)
assertEquals(false, retrieved2[1]?.input)
}
}
76 changes: 71 additions & 5 deletions android/src/main/java/app/tauri/notification/Notification.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import android.content.ContentResolver
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build
import android.service.notification.StatusBarNotification
import androidx.annotation.RequiresApi
import app.tauri.annotation.InvokeArg
import app.tauri.plugin.JSArray
import app.tauri.plugin.JSObject
import org.json.JSONException
import org.json.JSONObject

@InvokeArg
class Notification {
Expand All @@ -38,6 +38,7 @@ class Notification {
var sourceJson: String? = null
var visibility: Int? = null
var number: Int? = null
var silent: Boolean? = null

fun getSound(context: Context, defaultSound: Int): String? {
var soundPath: String? = null
Expand Down Expand Up @@ -84,12 +85,77 @@ class Notification {
fun buildNotificationPendingList(notifications: List<Notification>): List<PendingNotification> {
val pendingNotifications = mutableListOf<PendingNotification>()
for (notification in notifications) {
val pendingNotification = PendingNotification(notification.id, notification.title, notification.body, notification.schedule, notification.extra)
val pendingNotification = PendingNotification(
id = notification.id,
title = notification.title,
body = notification.body,
schedule = notification.schedule,
extra = notification.extra
)
pendingNotifications.add(pendingNotification)
}
return pendingNotifications
}

@RequiresApi(Build.VERSION_CODES.M)
fun buildNotificationActiveList(statusBarNotifications: Array<StatusBarNotification>): List<ActiveNotificationInfo> {
val activeNotifications = mutableListOf<ActiveNotificationInfo>()
for (statusBarNotification in statusBarNotifications) {
val notification = statusBarNotification.notification
val data = mutableMapOf<String, String>()
if (notification != null) {
for (key in notification.extras.keySet()) {
notification.extras.getString(key)?.let { value ->
data[key] = value
}
}
}

val activeNotification = ActiveNotificationInfo(
id = statusBarNotification.id,
tag = statusBarNotification.tag,
title = notification?.extras?.getCharSequence(android.app.Notification.EXTRA_TITLE)?.toString(),
body = notification?.extras?.getCharSequence(android.app.Notification.EXTRA_TEXT)?.toString(),
group = notification?.group,
groupSummary = notification?.let { 0 != it.flags and android.app.Notification.FLAG_GROUP_SUMMARY } ?: false,
data = data,
extra = emptyMap(),
attachments = emptyList(),
actionTypeId = null,
schedule = null,
sound = null
)
activeNotifications.add(activeNotification)
}
return activeNotifications
}
}
}

class PendingNotification(val id: Int, val title: String?, val body: String?, val schedule: NotificationSchedule?, val extra: JSObject?)
class PendingNotification(
val id: Int,
val title: String?,
val body: String?,
val schedule: NotificationSchedule?,
val extra: JSObject?
)

class ActiveNotificationInfo(
val id: Int,
val tag: String?,
val title: String?,
val body: String?,
val group: String?,
val groupSummary: Boolean,
val data: Map<String, String>,
val extra: Map<String, Any>,
val attachments: List<AttachmentInfo>,
val actionTypeId: String?,
val schedule: NotificationSchedule?,
val sound: String?
)

class AttachmentInfo(
val id: String,
val url: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@

package app.tauri.notification

import app.tauri.Logger
import app.tauri.plugin.JSObject
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject

private const val ATTACHMENT_TAG = "NotificationAttachment"

class NotificationAttachment {
var id: String? = null
var url: String? = null
Expand All @@ -20,7 +23,8 @@ class NotificationAttachment {
var attachments: JSONArray? = null
try {
attachments = notification.getJSONArray("attachments")
} catch (_: Exception) {
} catch (e: Exception) {
Logger.debug(Logger.tags(ATTACHMENT_TAG), "No attachments found in notification: ${e.message}")
}
if (attachments != null) {
for (i in 0 until attachments.length()) {
Expand All @@ -29,18 +33,21 @@ class NotificationAttachment {
try {
jsonObject = attachments.getJSONObject(i)
} catch (e: JSONException) {
Logger.error(Logger.tags(ATTACHMENT_TAG), "Failed to get attachment object at index $i: ${e.message}", e)
}
if (jsonObject != null) {
var jsObject: JSObject? = null
try {
jsObject = JSObject.fromJSONObject(jsonObject)
} catch (_: JSONException) {
} catch (e: JSONException) {
Logger.error(Logger.tags(ATTACHMENT_TAG), "Failed to convert attachment JSON object: ${e.message}", e)
}
newAttachment.id = jsObject!!.getString("id")
newAttachment.url = jsObject.getString("url")
try {
newAttachment.options = jsObject.getJSONObject("options")
} catch (_: JSONException) {
} catch (e: JSONException) {
Logger.debug(Logger.tags(ATTACHMENT_TAG), "No options found for attachment: ${e.message}")
}
attachmentsList.add(newAttachment)
}
Expand Down
Loading
Loading