├── CHANGELOG.md
├── Cargo.toml
├── LICENSE.spdx
├── LICENSE_APACHE-2.0
├── LICENSE_MIT
├── README.md
├── SECURITY.md
├── android
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
├── settings.gradle
└── src
│ ├── androidTest
│ └── java
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ ├── AssetUtils.kt
│ │ ├── ChannelManager.kt
│ │ ├── Notification.kt
│ │ ├── NotificationAttachment.kt
│ │ ├── NotificationPlugin.kt
│ │ ├── NotificationSchedule.kt
│ │ ├── NotificationStorage.kt
│ │ └── TauriNotificationManager.kt
│ └── res
│ │ └── drawable
│ │ └── ic_transparent.xml
│ └── test
│ └── java
│ └── ExampleUnitTest.kt
├── api-iife.js
├── banner.png
├── build.rs
├── dist-js
├── index.cjs
├── index.d.ts
├── index.js
└── init.d.ts
├── guest-js
├── index.ts
└── init.ts
├── ios
├── .gitignore
├── Package.swift
├── README.md
├── Sources
│ ├── Notification.swift
│ ├── NotificationCategory.swift
│ ├── NotificationHandler.swift
│ ├── NotificationManager.swift
│ └── NotificationPlugin.swift
└── Tests
│ └── PluginTests
│ └── PluginTests.swift
├── node_modules
└── @tauri-apps
│ └── api
├── package.json
├── permissions
├── autogenerated
│ ├── commands
│ │ ├── batch.toml
│ │ ├── cancel.toml
│ │ ├── check_permissions.toml
│ │ ├── create_channel.toml
│ │ ├── delete_channel.toml
│ │ ├── get_active.toml
│ │ ├── get_pending.toml
│ │ ├── is_permission_granted.toml
│ │ ├── list_channels.toml
│ │ ├── notify.toml
│ │ ├── permission_state.toml
│ │ ├── register_action_types.toml
│ │ ├── register_listener.toml
│ │ ├── remove_active.toml
│ │ ├── request_permission.toml
│ │ └── show.toml
│ └── reference.md
├── default.toml
└── schemas
│ └── schema.json
├── rollup.config.js
├── src
├── commands.rs
├── desktop.rs
├── error.rs
├── init-iife.js
├── lib.rs
├── mobile.rs
└── models.rs
├── test
└── tauri.conf.json
└── tsconfig.json
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## \[2.2.2]
4 |
5 | - [`a1b3fa27`](https://github.com/tauri-apps/plugins-workspace/commit/a1b3fa27f11022c9b6622b4fab12d93239eb05de) ([#2515](https://github.com/tauri-apps/plugins-workspace/pull/2515) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Re-exported the `Geolocation`, `Haptics`, `Notification`, and `Os` structs so that they show up on docs.rs.
6 |
7 | ## \[2.2.1]
8 |
9 | - [`da5c59e2`](https://github.com/tauri-apps/plugins-workspace/commit/da5c59e2fe879d177e3cfd52fcacce85440423cb) ([#2271](https://github.com/tauri-apps/plugins-workspace/pull/2271) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated `zbus` dependency to version 5. No API changes.
10 |
11 | ## \[2.2.0]
12 |
13 | - [`3a79266b`](https://github.com/tauri-apps/plugins-workspace/commit/3a79266b8cf96a55b1ae6339d725567d45a44b1d) ([#2173](https://github.com/tauri-apps/plugins-workspace/pull/2173) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Bumped all plugins to `v2.2.0`. From now, the versions for the Rust and JavaScript packages of each plugin will be in sync with each other.
14 |
15 | ## \[2.0.1]
16 |
17 | - [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7.
18 |
19 | ## \[2.0.0]
20 |
21 | - [`e2c4dfb6`](https://github.com/tauri-apps/plugins-workspace/commit/e2c4dfb6af43e5dd8d9ceba232c315f5febd55c1) Update to tauri v2 stable release.
22 |
23 | ## \[2.0.0-rc.5]
24 |
25 | - [`fb85e5dd`](https://github.com/tauri-apps/plugins-workspace/commit/fb85e5dd76688f3ae836890160f9bde843b70167) ([#1785](https://github.com/tauri-apps/plugins-workspace/pull/1785)) Update to tauri 2.0.0-rc.12.
26 |
27 | ## \[2.0.0-rc.4]
28 |
29 | - [`3d301c65`](https://github.com/tauri-apps/plugins-workspace/commit/3d301c654e6f5e7f343e0e0cbb57648002e98f04) ([#1737](https://github.com/tauri-apps/plugins-workspace/pull/1737) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) The notification body is now optional on iOS to match the other platforms.
30 |
31 | ## \[2.0.0-rc.1]
32 |
33 | - [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Use `PermissionState` from the `tauri` crate, which now also includes a "prompt with rationale" variant for Android (returned when your app must explain to the user why it needs the permission).
34 | - [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) **Breaking change**: The permission type when using the API is now `'granted' | 'denied' | 'prompt' | 'prompt-with-rationale'` instead of `'granted' | 'denied' | 'default'` for consistency with Rust types. When using the `window.Notification` API the type is unchanged to match the Web API type.
35 | - [`e2e97db5`](https://github.com/tauri-apps/plugins-workspace/commit/e2e97db51983267f5be84d4f6f0278d58834d1f5) ([#1701](https://github.com/tauri-apps/plugins-workspace/pull/1701) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri 2.0.0-rc.8
36 |
37 | ## \[2.0.0-rc.2]
38 |
39 | - [`b9147758`](https://github.com/tauri-apps/plugins-workspace/commit/b914775898c2bee7ceb20bd17ee595005cd17a64) ([#1679](https://github.com/tauri-apps/plugins-workspace/pull/1679) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Explicitly set a minimum macOS version for the Swift package.
40 |
41 | ## \[2.0.0-rc.1]
42 |
43 | ### changes
44 |
45 | - [`6b079cfd`](https://github.com/tauri-apps/plugins-workspace/commit/6b079cfdd107c94abc2c7300f6af00bac3ff4040) ([#1649](https://github.com/tauri-apps/plugins-workspace/pull/1649) by [@ahqsoftwares](https://github.com/tauri-apps/plugins-workspace/../../ahqsoftwares)) Remove targetSdk from build.kts files as it is deprecated and will be removed from DSL v9.0
46 |
47 | ## \[2.0.0-rc.0]
48 |
49 | - [`9887d1`](https://github.com/tauri-apps/plugins-workspace/commit/9887d14bd0e971c4c0f5c1188fc4005d3fc2e29e) Update to tauri RC.
50 |
51 | ## \[2.0.0-beta.8]
52 |
53 | - [`99d6ac0f`](https://github.com/tauri-apps/plugins-workspace/commit/99d6ac0f9506a6a4a1aa59c728157190a7441af6) ([#1606](https://github.com/tauri-apps/plugins-workspace/pull/1606) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) The JS packages now specify the *minimum* `@tauri-apps/api` version instead of a single exact version.
54 | - [`6de87966`](https://github.com/tauri-apps/plugins-workspace/commit/6de87966ecc00ad9d91c25be452f1f46bd2b7e1f) ([#1597](https://github.com/tauri-apps/plugins-workspace/pull/1597) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Update to tauri beta.25.
55 |
56 | ## \[2.0.0-beta.11]
57 |
58 | - [`725ff429`](https://github.com/tauri-apps/plugins-workspace/commit/725ff4295e56df9c30c099813bd64b96fe61b945) ([#1556](https://github.com/tauri-apps/plugins-workspace/pull/1556) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) Fixed an issue that caused the `notification` plugin's initialization script to cause the WebView on Windows to throw a `STATUS_ACCESS_VIOLATION` error on remote websites.
59 |
60 | ## \[2.0.0-beta.7]
61 |
62 | - [`22a17980`](https://github.com/tauri-apps/plugins-workspace/commit/22a17980ff4f6f8c40adb1b8f4ffc6dae2fe7e30) ([#1537](https://github.com/tauri-apps/plugins-workspace/pull/1537) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Update to tauri beta.24.
63 |
64 | ## \[2.0.0-beta.6]
65 |
66 | - [`76daee7a`](https://github.com/tauri-apps/plugins-workspace/commit/76daee7aafece34de3092c86e531cf9eb1138989) ([#1512](https://github.com/tauri-apps/plugins-workspace/pull/1512) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Update to tauri beta.23.
67 |
68 | ## \[2.0.0-beta.8]
69 |
70 | - [`3779fb50`](https://github.com/tauri-apps/plugins-workspace/commit/3779fb50634fba4d7e7eb0bfecc2216349b9d64d) ([#1432](https://github.com/tauri-apps/plugins-workspace/pull/1432) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Use notify_rust from crates.io instead of local fork.
71 |
72 | ## \[2.0.0-beta.5]
73 |
74 | - [`9013854f`](https://github.com/tauri-apps/plugins-workspace/commit/9013854f42a49a230b9dbb9d02774765528a923f)([#1382](https://github.com/tauri-apps/plugins-workspace/pull/1382)) Update to tauri beta.22.
75 |
76 | ## \[2.0.0-beta.4]
77 |
78 | - [`430bd6f4`](https://github.com/tauri-apps/plugins-workspace/commit/430bd6f4f379bee5d232ae6b098ae131db7f178a)([#1363](https://github.com/tauri-apps/plugins-workspace/pull/1363)) Update to tauri beta.20.
79 |
80 | ## \[2.0.0-beta.3]
81 |
82 | - [`bd1ed590`](https://github.com/tauri-apps/plugins-workspace/commit/bd1ed5903ffcce5500310dac1e59e8c67674ef1e)([#1237](https://github.com/tauri-apps/plugins-workspace/pull/1237)) Update to tauri beta.17.
83 |
84 | ## \[2.0.0-beta.4]
85 |
86 | - [`326df688`](https://github.com/tauri-apps/plugins-workspace/commit/326df6883998d416fc0837583ed972854628bb52)([#1236](https://github.com/tauri-apps/plugins-workspace/pull/1236)) Fixes command argument parsing on iOS.
87 |
88 | ## \[2.0.0-beta.3]
89 |
90 | - [`a04ea2f`](https://github.com/tauri-apps/plugins-workspace/commit/a04ea2f38294d5a3987578283badc8eec87a7752)([#1071](https://github.com/tauri-apps/plugins-workspace/pull/1071)) The global API script is now only added to the binary when the `withGlobalTauri` config is true.
91 | - [`62ce5df`](https://github.com/tauri-apps/plugins-workspace/commit/62ce5df52ca3c86786e711ef193a206e7b0dc0cf)([#1096](https://github.com/tauri-apps/plugins-workspace/pull/1096)) Fix development mode check to set the app ID on macOS.
92 |
93 | ## \[2.0.0-beta.2]
94 |
95 | - [`99bea25`](https://github.com/tauri-apps/plugins-workspace/commit/99bea2559c2c0648c2519c50a18cd124dacef57b)([#1005](https://github.com/tauri-apps/plugins-workspace/pull/1005)) Update to tauri beta.8.
96 |
97 | ## \[2.0.0-beta.1]
98 |
99 | - [`569defb`](https://github.com/tauri-apps/plugins-workspace/commit/569defbe9492e38938554bb7bdc1be9151456d21) Update to tauri beta.4.
100 |
101 | ## \[2.0.0-beta.0]
102 |
103 | - [`d198c01`](https://github.com/tauri-apps/plugins-workspace/commit/d198c014863ee260cb0de88a14b7fc4356ef7474)([#862](https://github.com/tauri-apps/plugins-workspace/pull/862)) Update to tauri beta.
104 | - [`1b1d795`](https://github.com/tauri-apps/plugins-workspace/commit/1b1d795b5866e5524a9a9925f0fb7b2f8e3e3675)([#874](https://github.com/tauri-apps/plugins-workspace/pull/874)) Export the missing `Schedule` class.
105 | - [`8dea78a`](https://github.com/tauri-apps/plugins-workspace/commit/8dea78ac7dcb502159e66bad464094696aa257d4)([#909](https://github.com/tauri-apps/plugins-workspace/pull/909)) Fixes deserialization and implementation bugs with scheduled notifications on Android.
106 |
107 | ## \[2.0.0-alpha.5]
108 |
109 | - [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.13.
110 |
111 | ## \[2.0.0-alpha.4]
112 |
113 | - [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.12.
114 |
115 | ## \[2.0.0-alpha.3]
116 |
117 | - [`e438e0a`](https://github.com/tauri-apps/plugins-workspace/commit/e438e0a62d4b430a5159f05f13ecd397dd891a0d)([#676](https://github.com/tauri-apps/plugins-workspace/pull/676)) Update to @tauri-apps/api v2.0.0-alpha.11.
118 |
119 | ## \[2.0.0-alpha.2]
120 |
121 | - [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9.
122 |
123 | ## \[2.0.0-alpha.3]
124 |
125 | - [`4e2cef9`](https://github.com/tauri-apps/plugins-workspace/commit/4e2cef9b702bbbb9cf4ee17de50791cb21f1b2a4)([#593](https://github.com/tauri-apps/plugins-workspace/pull/593)) Update to alpha.12.
126 |
127 | ## \[2.0.0-alpha.1]
128 |
129 | - [`d74fc0a`](https://github.com/tauri-apps/plugins-workspace/commit/d74fc0a097996e90a37be8f57d50b7d1f6ca616f)([#555](https://github.com/tauri-apps/plugins-workspace/pull/555)) Update to alpha.11.
130 |
131 | ## \[2.0.0-alpha.1]
132 |
133 | - [`d8b4aca`](https://github.com/tauri-apps/plugins-workspace/commit/d8b4aca69f628b170804ecb982e2c319d026ef47)([#414](https://github.com/tauri-apps/plugins-workspace/pull/414)) Use `window.__TAURI_INVOKE__` instead of `window.__TAURI__` in init.js, fixes usage in apps without `withGlobalTauri` enabled.
134 | - [`7d71ad4`](https://github.com/tauri-apps/plugins-workspace/commit/7d71ad4e587bcf47ea34645f5b226945e487b765) Play a default sound when showing a notification on Windows.
135 |
136 | ## \[2.0.0-alpha.0]
137 |
138 | - [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
139 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "tauri-plugin-notification"
3 | version = "2.2.2"
4 | description = "Send desktop and mobile notifications on your Tauri application."
5 | edition = { workspace = true }
6 | authors = { workspace = true }
7 | license = { workspace = true }
8 | rust-version = { workspace = true }
9 | repository = { workspace = true }
10 | links = "tauri-plugin-notification"
11 |
12 | [package.metadata.docs.rs]
13 | rustc-args = ["--cfg", "docsrs"]
14 | rustdoc-args = ["--cfg", "docsrs"]
15 | targets = ["x86_64-unknown-linux-gnu", "x86_64-linux-android"]
16 |
17 | [package.metadata.platforms.support]
18 | windows = { level = "full", notes = "Only works for installed apps. Shows powershell name & icon in development." }
19 | linux = { level = "full", notes = "" }
20 | macos = { level = "full", notes = "" }
21 | android = { level = "full", notes = "" }
22 | ios = { level = "full", notes = "" }
23 |
24 | [build-dependencies]
25 | tauri-plugin = { workspace = true, features = ["build"] }
26 |
27 | [dependencies]
28 | serde = { workspace = true }
29 | serde_json = { workspace = true }
30 | tauri = { workspace = true }
31 | log = { workspace = true }
32 | thiserror = { workspace = true }
33 | rand = "0.8"
34 | time = { version = "0.3", features = ["serde", "parsing", "formatting"] }
35 | url = { version = "2", features = ["serde"] }
36 | serde_repr = "0.1"
37 |
38 | [target.'cfg(target_os = "ios")'.dependencies]
39 | tauri = { workspace = true, features = ["wry"] }
40 |
41 | [target."cfg(windows)".dependencies]
42 | win7-notifications = { version = "0.4.5", optional = true }
43 | windows-version = { version = "0.1", optional = true }
44 |
45 | [target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
46 | notify-rust = "4.11"
47 |
48 | [dev-dependencies]
49 | color-backtrace = "0.7"
50 | ctor = "0.2"
51 | maplit = "1"
52 |
53 | [features]
54 | windows7-compat = ["win7-notifications", "windows-version"]
55 |
--------------------------------------------------------------------------------
/LICENSE.spdx:
--------------------------------------------------------------------------------
1 | SPDXVersion: SPDX-2.1
2 | DataLicense: CC0-1.0
3 | PackageName: tauri
4 | DataFormat: SPDXRef-1
5 | PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy
6 | PackageHomePage: https://tauri.app
7 | PackageLicenseDeclared: Apache-2.0
8 | PackageLicenseDeclared: MIT
9 | PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy
10 | PackageSummary: Tauri is a rust project that enables developers to make secure
11 | and small desktop applications using a web frontend.
12 |
13 | PackageComment: The package includes the following libraries; see
14 | Relationship information.
15 |
16 | Created: 2019-05-20T09:00:00Z
17 | PackageDownloadLocation: git://github.com/tauri-apps/tauri
18 | PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git
19 | PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git
20 | Creator: Person: Daniel Thompson-Yvetot
--------------------------------------------------------------------------------
/LICENSE_APACHE-2.0:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
--------------------------------------------------------------------------------
/LICENSE_MIT:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 - Present Tauri Apps Contributors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | Send message notifications (brief auto-expiring OS window element) to your user. Can also be used with the Notification Web API.
4 |
5 | | Platform | Supported |
6 | | -------- | --------- |
7 | | Linux | ✓ |
8 | | Windows | ✓ |
9 | | macOS | ✓ |
10 | | Android | ✓ |
11 | | iOS | ✓ |
12 |
13 | ## Install
14 |
15 | _This plugin requires a Rust version of at least **1.77.2**_
16 |
17 | There are three general methods of installation that we can recommend.
18 |
19 | 1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)
20 | 2. Pull sources directly from Github using git tags / revision hashes (most secure)
21 | 3. Git submodule install this repo in your tauri project and then use file protocol to ingest the source (most secure, but inconvenient to use)
22 |
23 | Install the Core plugin by adding the following to your `Cargo.toml` file:
24 |
25 | `src-tauri/Cargo.toml`
26 |
27 | ```toml
28 | [dependencies]
29 | tauri-plugin-notification = "2.0.0"
30 | # alternatively with Git:
31 | tauri-plugin-notification = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
32 | ```
33 |
34 | You can install the JavaScript Guest bindings using your preferred JavaScript package manager:
35 |
36 | > Note: Since most JavaScript package managers are unable to install packages from git monorepos we provide read-only mirrors of each plugin. This makes installation option 2 more ergonomic to use.
37 |
38 | ```sh
39 | pnpm add @tauri-apps/plugin-notification
40 | # or
41 | npm add @tauri-apps/plugin-notification
42 | # or
43 | yarn add @tauri-apps/plugin-notification
44 |
45 | # alternatively with Git:
46 | pnpm add https://github.com/tauri-apps/tauri-plugin-notification#v2
47 | # or
48 | npm add https://github.com/tauri-apps/tauri-plugin-notification#v2
49 | # or
50 | yarn add https://github.com/tauri-apps/tauri-plugin-notification#v2
51 | ```
52 |
53 | ## Usage
54 |
55 | First you need to register the core plugin with Tauri:
56 |
57 | `src-tauri/src/lib.rs`
58 |
59 | ```rust
60 | fn main() {
61 | tauri::Builder::default()
62 | .plugin(tauri_plugin_notification::init())
63 | .run(tauri::generate_context!())
64 | .expect("error while running tauri application");
65 | }
66 | ```
67 |
68 | Then you need to add the permissions to your capabilities file:
69 |
70 | `src-tauri/capabilities/main.json`
71 |
72 | ```json
73 | {
74 | ...
75 | "permissions": [
76 | ...
77 | "notification:default"
78 | ],
79 | ...
80 | }
81 | ```
82 |
83 | Afterwards all the plugin's APIs are available through the JavaScript guest bindings:
84 |
85 | ```javascript
86 | import {
87 | isPermissionGranted,
88 | requestPermission,
89 | sendNotification
90 | } from '@tauri-apps/plugin-notification'
91 |
92 | async function checkPermission() {
93 | if (!(await isPermissionGranted())) {
94 | return (await requestPermission()) === 'granted'
95 | }
96 | return true
97 | }
98 |
99 | export async function enqueueNotification(title, body) {
100 | if (!(await checkPermission())) {
101 | return
102 | }
103 | sendNotification({ title, body })
104 | }
105 | ```
106 |
107 | ## Contributing
108 |
109 | PRs accepted. Please make sure to read the Contributing Guide before making a pull request.
110 |
111 | ## Partners
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri).
126 |
127 | ## License
128 |
129 | Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy.
130 |
131 | MIT or MIT/Apache 2.0 where applicable.
132 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | **Do not report security vulnerabilities through public GitHub issues.**
4 |
5 | **Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.**
6 |
7 | Include as much of the following information:
8 |
9 | - Type of issue (e.g. improper input parsing, privilege escalation, etc.)
10 | - The location of the affected source code (tag/branch/commit or direct URL)
11 | - Any special configuration required to reproduce the issue
12 | - The distribution affected or used to help us with reproduction of the issue
13 | - Step-by-step instructions to reproduce the issue
14 | - Ideally a reproduction repository
15 | - Impact of the issue, including how an attacker might exploit the issue
16 |
17 | We prefer to receive reports in English.
18 |
19 | ## Contact
20 |
21 | Please disclose a vulnerability or security relevant issue here: [https://github.com/tauri-apps/plugins-workspace/security/advisories/new](https://github.com/tauri-apps/plugins-workspace/security/advisories/new).
22 |
23 | Alternatively, you can also contact us by email via [security@tauri.app](mailto:security@tauri.app).
24 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /.tauri
3 |
--------------------------------------------------------------------------------
/android/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | id("org.jetbrains.kotlin.android")
4 | }
5 |
6 | android {
7 | namespace = "app.tauri.notification"
8 | compileSdk = 34
9 |
10 | defaultConfig {
11 | minSdk = 24
12 |
13 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
14 | consumerProguardFiles("consumer-rules.pro")
15 | }
16 |
17 | buildTypes {
18 | release {
19 | isMinifyEnabled = false
20 | proguardFiles(
21 | getDefaultProguardFile("proguard-android-optimize.txt"),
22 | "proguard-rules.pro"
23 | )
24 | }
25 | }
26 | compileOptions {
27 | sourceCompatibility = JavaVersion.VERSION_1_8
28 | targetCompatibility = JavaVersion.VERSION_1_8
29 | }
30 | kotlinOptions {
31 | jvmTarget = "1.8"
32 | }
33 | }
34 |
35 | dependencies {
36 |
37 | implementation("androidx.core:core-ktx:1.9.0")
38 | implementation("androidx.appcompat:appcompat:1.6.0")
39 | implementation("com.google.android.material:material:1.7.0")
40 | implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3")
41 | testImplementation("junit:junit:4.13.2")
42 | androidTestImplementation("androidx.test.ext:junit:1.1.5")
43 | androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
44 | implementation(project(":tauri-android"))
45 | }
46 |
--------------------------------------------------------------------------------
/android/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':tauri-android'
2 | project(':tauri-android').projectDir = new File('./.tauri/tauri-api')
3 |
--------------------------------------------------------------------------------
/android/src/androidTest/java/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | package app.tauri.notification
6 |
7 | import androidx.test.platform.app.InstrumentationRegistry
8 | import androidx.test.ext.junit.runners.AndroidJUnit4
9 |
10 | import org.junit.Test
11 | import org.junit.runner.RunWith
12 |
13 | import org.junit.Assert.*
14 |
15 | /**
16 | * Instrumented test, which will execute on an Android device.
17 | *
18 | * See [testing documentation](http://d.android.com/tools/testing).
19 | */
20 | @RunWith(AndroidJUnit4::class)
21 | class ExampleInstrumentedTest {
22 | @Test
23 | fun useAppContext() {
24 | // Context of the app under test.
25 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
26 | assertEquals("app.tauri.notification", appContext.packageName)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/android/src/main/java/AssetUtils.kt:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | package app.tauri.notification
6 |
7 | import android.annotation.SuppressLint
8 | import android.content.Context
9 |
10 | class AssetUtils {
11 | companion object {
12 | const val RESOURCE_ID_ZERO_VALUE = 0
13 |
14 | @SuppressLint("DiscouragedApi")
15 | fun getResourceID(context: Context, resourceName: String?, dir: String?): Int {
16 | return context.resources.getIdentifier(resourceName, dir, context.packageName)
17 | }
18 |
19 | fun getResourceBaseName(resPath: String?): String? {
20 | if (resPath == null) return null
21 | if (resPath.contains("/")) {
22 | return resPath.substring(resPath.lastIndexOf('/') + 1)
23 | }
24 | return if (resPath.contains(".")) {
25 | resPath.substring(0, resPath.lastIndexOf('.'))
26 | } else resPath
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/android/src/main/java/ChannelManager.kt:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | package app.tauri.notification
6 |
7 | import android.app.NotificationChannel
8 | import android.app.NotificationManager
9 | import android.content.ContentResolver
10 | import android.content.Context
11 | import android.graphics.Color
12 | import android.media.AudioAttributes
13 | import android.net.Uri
14 | import android.os.Build
15 | import app.tauri.Logger
16 | import app.tauri.annotation.InvokeArg
17 | import app.tauri.plugin.Invoke
18 | import com.fasterxml.jackson.annotation.JsonValue
19 |
20 | enum class Importance(@JsonValue val value: Int) {
21 | None(0),
22 | Min(1),
23 | Low(2),
24 | Default(3),
25 | High(4);
26 | }
27 |
28 | enum class Visibility(@JsonValue val value: Int) {
29 | Secret(-1),
30 | Private(0),
31 | Public(1);
32 | }
33 |
34 | @InvokeArg
35 | class Channel {
36 | lateinit var id: String
37 | lateinit var name: String
38 | var description: String? = null
39 | var sound: String? = null
40 | var lights: Boolean? = null
41 | var lightsColor: String? = null
42 | var vibration: Boolean? = null
43 | var importance: Importance? = null
44 | var visibility: Visibility? = null
45 | }
46 |
47 | @InvokeArg
48 | class DeleteChannelArgs {
49 | lateinit var id: String
50 | }
51 |
52 | class ChannelManager(private var context: Context) {
53 | private var notificationManager: NotificationManager? = null
54 |
55 | init {
56 | notificationManager =
57 | context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager?
58 | }
59 |
60 | fun createChannel(invoke: Invoke) {
61 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
62 | val channel = invoke.parseArgs(Channel::class.java)
63 | createChannel(channel)
64 | invoke.resolve()
65 | } else {
66 | invoke.reject("channel not available")
67 | }
68 | }
69 |
70 | private fun createChannel(channel: Channel) {
71 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
72 | val notificationChannel = NotificationChannel(
73 | channel.id,
74 | channel.name,
75 | (channel.importance ?: Importance.Default).value
76 | )
77 | notificationChannel.description = channel.description
78 | notificationChannel.lockscreenVisibility = (channel.visibility ?: Visibility.Private).value
79 | notificationChannel.enableVibration(channel.vibration ?: false)
80 | notificationChannel.enableLights(channel.lights ?: false)
81 | val lightColor = channel.lightsColor ?: ""
82 | if (lightColor.isNotEmpty()) {
83 | try {
84 | notificationChannel.lightColor = Color.parseColor(lightColor)
85 | } catch (ex: IllegalArgumentException) {
86 | Logger.error(
87 | Logger.tags("NotificationChannel"),
88 | "Invalid color provided for light color.",
89 | null
90 | )
91 | }
92 | }
93 | var sound = channel.sound ?: ""
94 | if (sound.isNotEmpty()) {
95 | if (sound.contains(".")) {
96 | sound = sound.substring(0, sound.lastIndexOf('.'))
97 | }
98 | val audioAttributes = AudioAttributes.Builder()
99 | .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
100 | .setUsage(AudioAttributes.USAGE_NOTIFICATION)
101 | .build()
102 | val soundUri =
103 | Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/raw/" + sound)
104 | notificationChannel.setSound(soundUri, audioAttributes)
105 | }
106 | notificationManager?.createNotificationChannel(notificationChannel)
107 | }
108 | }
109 |
110 | fun deleteChannel(invoke: Invoke) {
111 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
112 | val args = invoke.parseArgs(DeleteChannelArgs::class.java)
113 | notificationManager?.deleteNotificationChannel(args.id)
114 | invoke.resolve()
115 | } else {
116 | invoke.reject("channel not available")
117 | }
118 | }
119 |
120 | fun listChannels(invoke: Invoke) {
121 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
122 | val notificationChannels: List =
123 | notificationManager?.notificationChannels ?: listOf()
124 |
125 | val channels = mutableListOf()
126 |
127 | for (notificationChannel in notificationChannels) {
128 | val channel = Channel()
129 | channel.id = notificationChannel.id
130 | channel.name = notificationChannel.name.toString()
131 | channel.description = notificationChannel.description
132 | channel.sound = notificationChannel.sound.toString()
133 | channel.lights = notificationChannel.shouldShowLights()
134 | String.format(
135 | "#%06X",
136 | 0xFFFFFF and notificationChannel.lightColor
137 | )
138 | channel.vibration = notificationChannel.shouldVibrate()
139 | channel.importance = Importance.values().firstOrNull { it.value == notificationChannel.importance }
140 | channel.visibility = Visibility.values().firstOrNull { it.value == notificationChannel.lockscreenVisibility }
141 |
142 | channels.add(channel)
143 | }
144 |
145 | invoke.resolveObject(channels)
146 |
147 | } else {
148 | invoke.reject("channel not available")
149 | }
150 | }
151 | }
--------------------------------------------------------------------------------
/android/src/main/java/Notification.kt:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | package app.tauri.notification
6 |
7 | import android.content.ContentResolver
8 | import android.content.Context
9 | import android.graphics.Bitmap
10 | import android.graphics.BitmapFactory
11 | import app.tauri.annotation.InvokeArg
12 | import app.tauri.plugin.JSArray
13 | import app.tauri.plugin.JSObject
14 | import org.json.JSONException
15 | import org.json.JSONObject
16 |
17 | @InvokeArg
18 | class Notification {
19 | var id: Int = 0
20 | var title: String? = null
21 | var body: String? = null
22 | var largeBody: String? = null
23 | var summary: String? = null
24 | var sound: String? = null
25 | var icon: String? = null
26 | var largeIcon: String? = null
27 | var iconColor: String? = null
28 | var actionTypeId: String? = null
29 | var group: String? = null
30 | var inboxLines: List? = null
31 | var isGroupSummary = false
32 | var isOngoing = false
33 | var isAutoCancel = false
34 | var extra: JSObject? = null
35 | var attachments: List? = null
36 | var schedule: NotificationSchedule? = null
37 | var channelId: String? = null
38 | var sourceJson: String? = null
39 | var visibility: Int? = null
40 | var number: Int? = null
41 |
42 | fun getSound(context: Context, defaultSound: Int): String? {
43 | var soundPath: String? = null
44 | var resId: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE
45 | val name = AssetUtils.getResourceBaseName(sound)
46 | if (name != null) {
47 | resId = AssetUtils.getResourceID(context, name, "raw")
48 | }
49 | if (resId == AssetUtils.RESOURCE_ID_ZERO_VALUE) {
50 | resId = defaultSound
51 | }
52 | if (resId != AssetUtils.RESOURCE_ID_ZERO_VALUE) {
53 | soundPath =
54 | ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + resId
55 | }
56 | return soundPath
57 | }
58 |
59 | fun getIconColor(globalColor: String): String {
60 | // use the one defined local before trying for a globally defined color
61 | return iconColor ?: globalColor
62 | }
63 |
64 | fun getSmallIcon(context: Context, defaultIcon: Int): Int {
65 | var resId: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE
66 | if (icon != null) {
67 | resId = AssetUtils.getResourceID(context, icon, "drawable")
68 | }
69 | if (resId == AssetUtils.RESOURCE_ID_ZERO_VALUE) {
70 | resId = defaultIcon
71 | }
72 | return resId
73 | }
74 |
75 | fun getLargeIcon(context: Context): Bitmap? {
76 | if (largeIcon != null) {
77 | val resId: Int = AssetUtils.getResourceID(context, largeIcon, "drawable")
78 | return BitmapFactory.decodeResource(context.resources, resId)
79 | }
80 | return null
81 | }
82 |
83 | companion object {
84 | fun buildNotificationPendingList(notifications: List): List {
85 | val pendingNotifications = mutableListOf()
86 | for (notification in notifications) {
87 | val pendingNotification = PendingNotification(notification.id, notification.title, notification.body, notification.schedule, notification.extra)
88 | pendingNotifications.add(pendingNotification)
89 | }
90 | return pendingNotifications
91 | }
92 | }
93 | }
94 |
95 | class PendingNotification(val id: Int, val title: String?, val body: String?, val schedule: NotificationSchedule?, val extra: JSObject?)
--------------------------------------------------------------------------------
/android/src/main/java/NotificationAttachment.kt:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | package app.tauri.notification
6 |
7 | import app.tauri.plugin.JSObject
8 | import org.json.JSONArray
9 | import org.json.JSONException
10 | import org.json.JSONObject
11 |
12 | class NotificationAttachment {
13 | var id: String? = null
14 | var url: String? = null
15 | var options: JSONObject? = null
16 |
17 | companion object {
18 | fun getAttachments(notification: JSObject): List {
19 | val attachmentsList: MutableList = ArrayList()
20 | var attachments: JSONArray? = null
21 | try {
22 | attachments = notification.getJSONArray("attachments")
23 | } catch (_: Exception) {
24 | }
25 | if (attachments != null) {
26 | for (i in 0 until attachments.length()) {
27 | val newAttachment = NotificationAttachment()
28 | var jsonObject: JSONObject? = null
29 | try {
30 | jsonObject = attachments.getJSONObject(i)
31 | } catch (e: JSONException) {
32 | }
33 | if (jsonObject != null) {
34 | var jsObject: JSObject? = null
35 | try {
36 | jsObject = JSObject.fromJSONObject(jsonObject)
37 | } catch (_: JSONException) {
38 | }
39 | newAttachment.id = jsObject!!.getString("id")
40 | newAttachment.url = jsObject.getString("url")
41 | try {
42 | newAttachment.options = jsObject.getJSONObject("options")
43 | } catch (_: JSONException) {
44 | }
45 | attachmentsList.add(newAttachment)
46 | }
47 | }
48 | }
49 | return attachmentsList
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/android/src/main/java/NotificationPlugin.kt:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | package app.tauri.notification
6 |
7 | import android.Manifest
8 | import android.annotation.SuppressLint
9 | import android.app.Activity
10 | import android.app.NotificationManager
11 | import android.content.Context
12 | import android.content.Intent
13 | import android.os.Build
14 | import android.webkit.WebView
15 | import app.tauri.PermissionState
16 | import app.tauri.annotation.Command
17 | import app.tauri.annotation.InvokeArg
18 | import app.tauri.annotation.Permission
19 | import app.tauri.annotation.PermissionCallback
20 | import app.tauri.annotation.TauriPlugin
21 | import app.tauri.plugin.Invoke
22 | import app.tauri.plugin.JSArray
23 | import app.tauri.plugin.JSObject
24 | import app.tauri.plugin.Plugin
25 |
26 | const val LOCAL_NOTIFICATIONS = "permissionState"
27 |
28 | @InvokeArg
29 | class PluginConfig {
30 | var icon: String? = null
31 | var sound: String? = null
32 | var iconColor: String? = null
33 | }
34 |
35 | @InvokeArg
36 | class BatchArgs {
37 | lateinit var notifications: List
38 | }
39 |
40 | @InvokeArg
41 | class CancelArgs {
42 | lateinit var notifications: List
43 | }
44 |
45 | @InvokeArg
46 | class NotificationAction {
47 | lateinit var id: String
48 | var title: String? = null
49 | var input: Boolean? = null
50 | }
51 |
52 | @InvokeArg
53 | class ActionType {
54 | lateinit var id: String
55 | lateinit var actions: List
56 | }
57 |
58 | @InvokeArg
59 | class RegisterActionTypesArgs {
60 | lateinit var types: List
61 | }
62 |
63 | @InvokeArg
64 | class ActiveNotification {
65 | var id: Int = 0
66 | var tag: String? = null
67 | }
68 |
69 | @InvokeArg
70 | class RemoveActiveArgs {
71 | var notifications: List = listOf()
72 | }
73 |
74 | @TauriPlugin(
75 | permissions = [
76 | Permission(strings = [Manifest.permission.POST_NOTIFICATIONS], alias = "permissionState")
77 | ]
78 | )
79 | class NotificationPlugin(private val activity: Activity): Plugin(activity) {
80 | private var webView: WebView? = null
81 | private lateinit var manager: TauriNotificationManager
82 | private lateinit var notificationManager: NotificationManager
83 | private lateinit var notificationStorage: NotificationStorage
84 | private var channelManager = ChannelManager(activity)
85 |
86 | companion object {
87 | var instance: NotificationPlugin? = null
88 |
89 | fun triggerNotification(notification: Notification) {
90 | instance?.triggerObject("notification", notification)
91 | }
92 | }
93 |
94 | override fun load(webView: WebView) {
95 | instance = this
96 |
97 | super.load(webView)
98 | this.webView = webView
99 | notificationStorage = NotificationStorage(activity, jsonMapper())
100 |
101 | val manager = TauriNotificationManager(
102 | notificationStorage,
103 | activity,
104 | activity,
105 | getConfig(PluginConfig::class.java)
106 | )
107 | manager.createNotificationChannel()
108 |
109 | this.manager = manager
110 |
111 | notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
112 |
113 | val intent = activity.intent
114 | intent?.let {
115 | onIntent(it)
116 | }
117 | }
118 |
119 | override fun onNewIntent(intent: Intent) {
120 | super.onNewIntent(intent)
121 | onIntent(intent)
122 | }
123 |
124 | fun onIntent(intent: Intent) {
125 | if (Intent.ACTION_MAIN != intent.action) {
126 | return
127 | }
128 | val dataJson = manager.handleNotificationActionPerformed(intent, notificationStorage)
129 | if (dataJson != null) {
130 | trigger("actionPerformed", dataJson)
131 | }
132 | }
133 |
134 | @Command
135 | fun show(invoke: Invoke) {
136 | val notification = invoke.parseArgs(Notification::class.java)
137 | val id = manager.schedule(notification)
138 |
139 | invoke.resolveObject(id)
140 | }
141 |
142 | @Command
143 | fun batch(invoke: Invoke) {
144 | val args = invoke.parseArgs(BatchArgs::class.java)
145 |
146 | val ids = manager.schedule(args.notifications)
147 | notificationStorage.appendNotifications(args.notifications)
148 |
149 | invoke.resolveObject(ids)
150 | }
151 |
152 | @Command
153 | fun cancel(invoke: Invoke) {
154 | val args = invoke.parseArgs(CancelArgs::class.java)
155 | manager.cancel(args.notifications)
156 | invoke.resolve()
157 | }
158 |
159 | @Command
160 | fun removeActive(invoke: Invoke) {
161 | val args = invoke.parseArgs(RemoveActiveArgs::class.java)
162 |
163 | if (args.notifications.isEmpty()) {
164 | notificationManager.cancelAll()
165 | invoke.resolve()
166 | } else {
167 | for (notification in args.notifications) {
168 | if (notification.tag == null) {
169 | notificationManager.cancel(notification.id)
170 | } else {
171 | notificationManager.cancel(notification.tag, notification.id)
172 | }
173 | }
174 | invoke.resolve()
175 | }
176 | }
177 |
178 | @Command
179 | fun getPending(invoke: Invoke) {
180 | val notifications= notificationStorage.getSavedNotifications()
181 | val result = Notification.buildNotificationPendingList(notifications)
182 | invoke.resolveObject(result)
183 | }
184 |
185 | @Command
186 | fun registerActionTypes(invoke: Invoke) {
187 | val args = invoke.parseArgs(RegisterActionTypesArgs::class.java)
188 | notificationStorage.writeActionGroup(args.types)
189 | invoke.resolve()
190 | }
191 |
192 | @SuppressLint("ObsoleteSdkInt")
193 | @Command
194 | fun getActive(invoke: Invoke) {
195 | val notifications = JSArray()
196 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
197 | val activeNotifications = notificationManager.activeNotifications
198 | for (activeNotification in activeNotifications) {
199 | val jsNotification = JSObject()
200 | jsNotification.put("id", activeNotification.id)
201 | jsNotification.put("tag", activeNotification.tag)
202 | val notification = activeNotification.notification
203 | if (notification != null) {
204 | jsNotification.put("title", notification.extras.getCharSequence(android.app.Notification.EXTRA_TITLE))
205 | jsNotification.put("body", notification.extras.getCharSequence(android.app.Notification.EXTRA_TEXT))
206 | jsNotification.put("group", notification.group)
207 | jsNotification.put(
208 | "groupSummary",
209 | 0 != notification.flags and android.app.Notification.FLAG_GROUP_SUMMARY
210 | )
211 | val extras = JSObject()
212 | for (key in notification.extras.keySet()) {
213 | extras.put(key!!, notification.extras.getString(key))
214 | }
215 | jsNotification.put("data", extras)
216 | }
217 | notifications.put(jsNotification)
218 | }
219 | }
220 |
221 | invoke.resolveObject(notifications)
222 | }
223 |
224 | @Command
225 | fun createChannel(invoke: Invoke) {
226 | channelManager.createChannel(invoke)
227 | }
228 |
229 | @Command
230 | fun deleteChannel(invoke: Invoke) {
231 | channelManager.deleteChannel(invoke)
232 | }
233 |
234 | @Command
235 | fun listChannels(invoke: Invoke) {
236 | channelManager.listChannels(invoke)
237 | }
238 |
239 | @Command
240 | override fun checkPermissions(invoke: Invoke) {
241 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
242 | val permissionsResultJSON = JSObject()
243 | permissionsResultJSON.put("permissionState", getPermissionState())
244 | invoke.resolve(permissionsResultJSON)
245 | } else {
246 | super.checkPermissions(invoke)
247 | }
248 | }
249 |
250 | @Command
251 | override fun requestPermissions(invoke: Invoke) {
252 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
253 | permissionState(invoke)
254 | } else {
255 | if (getPermissionState(LOCAL_NOTIFICATIONS) !== PermissionState.GRANTED) {
256 | requestPermissionForAlias(LOCAL_NOTIFICATIONS, invoke, "permissionsCallback")
257 | }
258 | }
259 | }
260 |
261 | @Command
262 | fun permissionState(invoke: Invoke) {
263 | val permissionsResultJSON = JSObject()
264 | permissionsResultJSON.put("permissionState", getPermissionState())
265 | invoke.resolve(permissionsResultJSON)
266 | }
267 |
268 | @PermissionCallback
269 | private fun permissionsCallback(invoke: Invoke) {
270 | val permissionsResultJSON = JSObject()
271 | permissionsResultJSON.put("permissionState", getPermissionState())
272 | invoke.resolve(permissionsResultJSON)
273 | }
274 |
275 | private fun getPermissionState(): String {
276 | return if (manager.areNotificationsEnabled()) {
277 | "granted"
278 | } else {
279 | "denied"
280 | }
281 | }
282 | }
283 |
--------------------------------------------------------------------------------
/android/src/main/java/NotificationSchedule.kt:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | package app.tauri.notification
6 |
7 | import android.annotation.SuppressLint
8 | import android.text.format.DateUtils
9 | import com.fasterxml.jackson.annotation.JsonFormat
10 | import com.fasterxml.jackson.annotation.JsonProperty
11 | import com.fasterxml.jackson.core.JsonGenerator
12 | import com.fasterxml.jackson.core.JsonParser
13 | import com.fasterxml.jackson.core.JsonProcessingException
14 | import com.fasterxml.jackson.databind.DeserializationContext
15 | import com.fasterxml.jackson.databind.JsonDeserializer
16 | import com.fasterxml.jackson.databind.JsonNode
17 | import com.fasterxml.jackson.databind.SerializerProvider
18 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize
19 | import com.fasterxml.jackson.databind.annotation.JsonSerialize
20 | import com.fasterxml.jackson.databind.deser.std.StdDeserializer
21 | import com.fasterxml.jackson.databind.ser.std.StdSerializer
22 | import java.io.IOException
23 | import java.text.SimpleDateFormat
24 | import java.util.Calendar
25 | import java.util.Date
26 | import java.util.TimeZone
27 |
28 | const val JS_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
29 |
30 | enum class NotificationInterval {
31 | @JsonProperty("year")
32 | Year,
33 | @JsonProperty("month")
34 | Month,
35 | @JsonProperty("twoWeeks")
36 | TwoWeeks,
37 | @JsonProperty("week")
38 | Week,
39 | @JsonProperty("day")
40 | Day,
41 | @JsonProperty("hour")
42 | Hour,
43 | @JsonProperty("minute")
44 | Minute,
45 | @JsonProperty("second")
46 | Second
47 | }
48 |
49 | fun getIntervalTime(interval: NotificationInterval, count: Int): Long {
50 | return when (interval) {
51 | // This case is just approximation as not all years have the same number of days
52 | NotificationInterval.Year -> count * DateUtils.WEEK_IN_MILLIS * 52
53 | // This case is just approximation as months have different number of days
54 | NotificationInterval.Month -> count * 30 * DateUtils.DAY_IN_MILLIS
55 | NotificationInterval.TwoWeeks -> count * 2 * DateUtils.WEEK_IN_MILLIS
56 | NotificationInterval.Week -> count * DateUtils.WEEK_IN_MILLIS
57 | NotificationInterval.Day -> count * DateUtils.DAY_IN_MILLIS
58 | NotificationInterval.Hour -> count * DateUtils.HOUR_IN_MILLIS
59 | NotificationInterval.Minute -> count * DateUtils.MINUTE_IN_MILLIS
60 | NotificationInterval.Second -> count * DateUtils.SECOND_IN_MILLIS
61 | }
62 | }
63 |
64 | @JsonDeserialize(using = NotificationScheduleDeserializer::class)
65 | @JsonSerialize(using = NotificationScheduleSerializer::class)
66 | sealed class NotificationSchedule {
67 | // At specific moment of time (with repeating option)
68 | @JsonDeserialize
69 | class At: NotificationSchedule() {
70 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = JS_DATE_FORMAT)
71 | lateinit var date: Date
72 | var repeating: Boolean = false
73 | var allowWhileIdle: Boolean = false
74 | }
75 | @JsonDeserialize
76 | class Interval: NotificationSchedule() {
77 | lateinit var interval: DateMatch
78 | var allowWhileIdle: Boolean = false
79 | }
80 | @JsonDeserialize
81 | class Every: NotificationSchedule() {
82 | lateinit var interval: NotificationInterval
83 | var count: Int = 0
84 | var allowWhileIdle: Boolean = false
85 | }
86 |
87 | fun isRemovable(): Boolean {
88 | return when (this) {
89 | is At -> !repeating
90 | else -> false
91 | }
92 | }
93 |
94 | fun allowWhileIdle(): Boolean {
95 | return when (this) {
96 | is At -> allowWhileIdle
97 | is Interval -> allowWhileIdle
98 | is Every -> allowWhileIdle
99 | else -> false
100 | }
101 | }
102 | }
103 |
104 | internal class NotificationScheduleSerializer @JvmOverloads constructor(t: Class? = null) :
105 | StdSerializer(t) {
106 | @SuppressLint("SimpleDateFormat")
107 | @Throws(IOException::class, JsonProcessingException::class)
108 | override fun serialize(
109 | value: NotificationSchedule, jgen: JsonGenerator, provider: SerializerProvider
110 | ) {
111 | jgen.writeStartObject()
112 | when (value) {
113 | is NotificationSchedule.At -> {
114 | jgen.writeObjectFieldStart("at")
115 |
116 | val sdf = SimpleDateFormat(JS_DATE_FORMAT)
117 | sdf.timeZone = TimeZone.getTimeZone("UTC")
118 | jgen.writeStringField("date", sdf.format(value.date))
119 | jgen.writeBooleanField("repeating", value.repeating)
120 |
121 | jgen.writeEndObject()
122 | }
123 | is NotificationSchedule.Interval -> {
124 | jgen.writeObjectFieldStart("interval")
125 |
126 | jgen.writeObjectField("interval", value.interval)
127 |
128 | jgen.writeEndObject()
129 | }
130 | is NotificationSchedule.Every -> {
131 | jgen.writeObjectFieldStart("every")
132 |
133 | jgen.writeObjectField("interval", value.interval)
134 | jgen.writeNumberField("count", value.count)
135 |
136 | jgen.writeEndObject()
137 | }
138 | }
139 |
140 | jgen.writeEndObject()
141 | }
142 | }
143 |
144 | internal class NotificationScheduleDeserializer: JsonDeserializer() {
145 | override fun deserialize(
146 | jsonParser: JsonParser,
147 | deserializationContext: DeserializationContext
148 | ): NotificationSchedule {
149 | val node: JsonNode = jsonParser.codec.readTree(jsonParser)
150 | node.get("at")?.let {
151 | return jsonParser.codec.treeToValue(it, NotificationSchedule.At::class.java)
152 | }
153 | node.get("interval")?.let {
154 | return jsonParser.codec.treeToValue(it, NotificationSchedule.Interval::class.java)
155 | }
156 | node.get("every")?.let {
157 | return jsonParser.codec.treeToValue(it, NotificationSchedule.Every::class.java)
158 | }
159 | throw Error("unknown schedule kind $node")
160 | }
161 | }
162 |
163 | class DateMatch {
164 | var year: Int? = null
165 | var month: Int? = null
166 | var day: Int? = null
167 | var weekday: Int? = null
168 | var hour: Int? = null
169 | var minute: Int? = null
170 | var second: Int? = null
171 |
172 | // Unit used to save the last used unit for a trigger.
173 | // One of the Calendar constants values
174 | var unit: Int? = -1
175 |
176 | /**
177 | * Gets a calendar instance pointing to the specified date.
178 | *
179 | * @param date The date to point.
180 | */
181 | private fun buildCalendar(date: Date): Calendar {
182 | val cal: Calendar = Calendar.getInstance()
183 | cal.time = date
184 | cal.set(Calendar.MILLISECOND, 0)
185 | return cal
186 | }
187 |
188 | /**
189 | * Calculates next trigger date for
190 | *
191 | * @param date base date used to calculate trigger
192 | * @return next trigger timestamp
193 | */
194 | fun nextTrigger(date: Date): Long {
195 | val current: Calendar = buildCalendar(date)
196 | val next: Calendar = buildNextTriggerTime(date)
197 | return postponeTriggerIfNeeded(current, next)
198 | }
199 |
200 | /**
201 | * Postpone trigger if first schedule matches the past
202 | */
203 | private fun postponeTriggerIfNeeded(current: Calendar, next: Calendar): Long {
204 | if (next.timeInMillis <= current.timeInMillis && unit != -1) {
205 | var incrementUnit = -1
206 | if (unit == Calendar.YEAR || unit == Calendar.MONTH) {
207 | incrementUnit = Calendar.YEAR
208 | } else if (unit == Calendar.DAY_OF_MONTH) {
209 | incrementUnit = Calendar.MONTH
210 | } else if (unit == Calendar.DAY_OF_WEEK) {
211 | incrementUnit = Calendar.WEEK_OF_MONTH
212 | } else if (unit == Calendar.HOUR_OF_DAY) {
213 | incrementUnit = Calendar.DAY_OF_MONTH
214 | } else if (unit == Calendar.MINUTE) {
215 | incrementUnit = Calendar.HOUR_OF_DAY
216 | } else if (unit == Calendar.SECOND) {
217 | incrementUnit = Calendar.MINUTE
218 | }
219 | if (incrementUnit != -1) {
220 | next.set(incrementUnit, next.get(incrementUnit) + 1)
221 | }
222 | }
223 | return next.timeInMillis
224 | }
225 |
226 | private fun buildNextTriggerTime(date: Date): Calendar {
227 | val next: Calendar = buildCalendar(date)
228 | if (year != null) {
229 | next.set(Calendar.YEAR, year ?: 0)
230 | if (unit == -1) unit = Calendar.YEAR
231 | }
232 | if (month != null) {
233 | next.set(Calendar.MONTH, month ?: 0)
234 | if (unit == -1) unit = Calendar.MONTH
235 | }
236 | if (day != null) {
237 | next.set(Calendar.DAY_OF_MONTH, day ?: 0)
238 | if (unit == -1) unit = Calendar.DAY_OF_MONTH
239 | }
240 | if (weekday != null) {
241 | next.set(Calendar.DAY_OF_WEEK, weekday ?: 0)
242 | if (unit == -1) unit = Calendar.DAY_OF_WEEK
243 | }
244 | if (hour != null) {
245 | next.set(Calendar.HOUR_OF_DAY, hour ?: 0)
246 | if (unit == -1) unit = Calendar.HOUR_OF_DAY
247 | }
248 | if (minute != null) {
249 | next.set(Calendar.MINUTE, minute ?: 0)
250 | if (unit == -1) unit = Calendar.MINUTE
251 | }
252 | if (second != null) {
253 | next.set(Calendar.SECOND, second ?: 0)
254 | if (unit == -1) unit = Calendar.SECOND
255 | }
256 | return next
257 | }
258 |
259 | override fun toString(): String {
260 | return "DateMatch{" +
261 | "year=" +
262 | year +
263 | ", month=" +
264 | month +
265 | ", day=" +
266 | day +
267 | ", weekday=" +
268 | weekday +
269 | ", hour=" +
270 | hour +
271 | ", minute=" +
272 | minute +
273 | ", second=" +
274 | second +
275 | '}'
276 | }
277 |
278 | override fun equals(other: Any?): Boolean {
279 | if (this === other) return true
280 | if (other == null || javaClass != other.javaClass) return false
281 | val dateMatch = other as DateMatch
282 | if (if (year != null) year != dateMatch.year else dateMatch.year != null) return false
283 | if (if (month != null) month != dateMatch.month else dateMatch.month != null) return false
284 | if (if (day != null) day != dateMatch.day else dateMatch.day != null) return false
285 | if (if (weekday != null) weekday != dateMatch.weekday else dateMatch.weekday != null) return false
286 | if (if (hour != null) hour != dateMatch.hour else dateMatch.hour != null) return false
287 | if (if (minute != null) minute != dateMatch.minute else dateMatch.minute != null) return false
288 | return if (second != null) second == dateMatch.second else dateMatch.second == null
289 | }
290 |
291 | override fun hashCode(): Int {
292 | var result = if (year != null) year.hashCode() else 0
293 | result = 31 * result + if (month != null) month.hashCode() else 0
294 | result = 31 * result + if (day != null) day.hashCode() else 0
295 | result = 31 * result + if (weekday != null) weekday.hashCode() else 0
296 | result = 31 * result + if (hour != null) hour.hashCode() else 0
297 | result = 31 * result + if (minute != null) minute.hashCode() else 0
298 | result += 31 + if (second != null) second.hashCode() else 0
299 | return result
300 | }
301 |
302 | /**
303 | * Transform DateMatch object to CronString
304 | *
305 | * @return
306 | */
307 | fun toMatchString(): String {
308 | val matchString = year.toString() +
309 | separator +
310 | month +
311 | separator +
312 | day +
313 | separator +
314 | weekday +
315 | separator +
316 | hour +
317 | separator +
318 | minute +
319 | separator +
320 | second +
321 | separator +
322 | unit
323 | return matchString.replace("null", "*")
324 | }
325 |
326 | companion object {
327 | private const val separator = " "
328 |
329 | /**
330 | * Create DateMatch object from stored string
331 | *
332 | * @param matchString
333 | * @return
334 | */
335 | fun fromMatchString(matchString: String): DateMatch {
336 | val date = DateMatch()
337 | val split = matchString.split(separator.toRegex()).dropLastWhile { it.isEmpty() }
338 | .toTypedArray()
339 | if (split.size == 7) {
340 | date.year = getValueFromCronElement(split[0])
341 | date.month = getValueFromCronElement(split[1])
342 | date.day = getValueFromCronElement(split[2])
343 | date.weekday = getValueFromCronElement(split[3])
344 | date.hour = getValueFromCronElement(split[4])
345 | date.minute = getValueFromCronElement(split[5])
346 | date.unit = getValueFromCronElement(split[6])
347 | }
348 | if (split.size == 8) {
349 | date.year = getValueFromCronElement(split[0])
350 | date.month = getValueFromCronElement(split[1])
351 | date.day = getValueFromCronElement(split[2])
352 | date.weekday = getValueFromCronElement(split[3])
353 | date.hour = getValueFromCronElement(split[4])
354 | date.minute = getValueFromCronElement(split[5])
355 | date.second = getValueFromCronElement(split[6])
356 | date.unit = getValueFromCronElement(split[7])
357 | }
358 | return date
359 | }
360 |
361 | private fun getValueFromCronElement(token: String): Int? {
362 | return try {
363 | token.toInt()
364 | } catch (e: NumberFormatException) {
365 | null
366 | }
367 | }
368 | }
369 | }
--------------------------------------------------------------------------------
/android/src/main/java/NotificationStorage.kt:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | package app.tauri.notification
6 |
7 | import android.content.Context
8 | import android.content.SharedPreferences
9 | import com.fasterxml.jackson.databind.ObjectMapper
10 | import org.json.JSONException
11 | import java.lang.Exception
12 |
13 | // Key for private preferences
14 | private const val NOTIFICATION_STORE_ID = "NOTIFICATION_STORE"
15 | // Key used to save action types
16 | private const val ACTION_TYPES_ID = "ACTION_TYPE_STORE"
17 |
18 | class NotificationStorage(private val context: Context, private val jsonMapper: ObjectMapper) {
19 | fun appendNotifications(localNotifications: List) {
20 | val storage = getStorage(NOTIFICATION_STORE_ID)
21 | val editor = storage.edit()
22 | for (request in localNotifications) {
23 | if (request.schedule != null) {
24 | val key: String = request.id.toString()
25 | editor.putString(key, request.sourceJson.toString())
26 | }
27 | }
28 | editor.apply()
29 | }
30 |
31 | fun getSavedNotificationIds(): List {
32 | val storage = getStorage(NOTIFICATION_STORE_ID)
33 | val all = storage.all
34 | return if (all != null) {
35 | ArrayList(all.keys)
36 | } else ArrayList()
37 | }
38 |
39 | fun getSavedNotifications(): List {
40 | val storage = getStorage(NOTIFICATION_STORE_ID)
41 | val all = storage.all
42 | if (all != null) {
43 | val notifications = ArrayList()
44 | for (key in all.keys) {
45 | val notificationString = all[key] as String?
46 | try {
47 | val notification = jsonMapper.readValue(notificationString, Notification::class.java)
48 | notifications.add(notification)
49 | } catch (_: Exception) { }
50 | }
51 | return notifications
52 | }
53 | return ArrayList()
54 | }
55 |
56 | fun getSavedNotification(key: String): Notification? {
57 | val storage = getStorage(NOTIFICATION_STORE_ID)
58 | val notificationString = try {
59 | storage.getString(key, null)
60 | } catch (ex: ClassCastException) {
61 | return null
62 | } ?: return null
63 |
64 | return try {
65 | jsonMapper.readValue(notificationString, Notification::class.java)
66 | } catch (ex: JSONException) {
67 | null
68 | }
69 | }
70 |
71 | fun deleteNotification(id: String?) {
72 | val editor = getStorage(NOTIFICATION_STORE_ID).edit()
73 | editor.remove(id)
74 | editor.apply()
75 | }
76 |
77 | private fun getStorage(key: String): SharedPreferences {
78 | return context.getSharedPreferences(key, Context.MODE_PRIVATE)
79 | }
80 |
81 | fun writeActionGroup(actions: List) {
82 | for (type in actions) {
83 | val i = type.id
84 | val editor = getStorage(ACTION_TYPES_ID + type.id).edit()
85 | editor.clear()
86 | editor.putInt("count", type.actions.size)
87 | for (action in type.actions) {
88 | editor.putString("id$i", action.id)
89 | editor.putString("title$i", action.title)
90 | editor.putBoolean("input$i", action.input ?: false)
91 | }
92 | editor.apply()
93 | }
94 | }
95 |
96 | fun getActionGroup(forId: String): Array {
97 | val storage = getStorage(ACTION_TYPES_ID + forId)
98 | val count = storage.getInt("count", 0)
99 | val actions: Array = arrayOfNulls(count)
100 | for (i in 0 until count) {
101 | val id = storage.getString("id$i", "")
102 | val title = storage.getString("title$i", "")
103 | val input = storage.getBoolean("input$i", false)
104 |
105 | val action = NotificationAction()
106 | action.id = id ?: ""
107 | action.title = title
108 | action.input = input
109 | actions[i] = action
110 | }
111 | return actions
112 | }
113 | }
--------------------------------------------------------------------------------
/android/src/main/res/drawable/ic_transparent.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/android/src/test/java/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | package app.tauri.notification
6 |
7 | import org.junit.Test
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Example local unit test, which will execute on the development machine (host).
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | class ExampleUnitTest {
17 | @Test
18 | fun addition_isCorrect() {
19 | assertEquals(4, 2 + 2)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/api-iife.js:
--------------------------------------------------------------------------------
1 | if("__TAURI__"in window){var __TAURI_PLUGIN_NOTIFICATION__=function(i){"use strict";function t(i,t,n,e){if("function"==typeof t||!t.has(i))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===n?e:"a"===n?e.call(i):e?e.value:t.get(i)}function n(i,t,n,e,a){if("function"==typeof t||!t.has(i))throw new TypeError("Cannot write private member to an object whose class did not declare it");return t.set(i,n),n}var e,a,o,r;"function"==typeof SuppressedError&&SuppressedError;const c="__TAURI_TO_IPC_KEY__";class s{constructor(i){e.set(this,void 0),a.set(this,0),o.set(this,[]),r.set(this,void 0),n(this,e,i||(()=>{})),this.id=function(i,t=!1){return window.__TAURI_INTERNALS__.transformCallback(i,t)}((i=>{const c=i.index;if("end"in i)return void(c==t(this,a,"f")?this.cleanupCallback():n(this,r,c));const s=i.message;if(c==t(this,a,"f")){for(t(this,e,"f").call(this,s),n(this,a,t(this,a,"f")+1);t(this,a,"f")in t(this,o,"f");){const i=t(this,o,"f")[t(this,a,"f")];t(this,e,"f").call(this,i),delete t(this,o,"f")[t(this,a,"f")],n(this,a,t(this,a,"f")+1)}t(this,a,"f")===t(this,r,"f")&&this.cleanupCallback()}else t(this,o,"f")[c]=s}))}cleanupCallback(){Reflect.deleteProperty(window,`_${this.id}`)}set onmessage(i){n(this,e,i)}get onmessage(){return t(this,e,"f")}[(e=new WeakMap,a=new WeakMap,o=new WeakMap,r=new WeakMap,c)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[c]()}}class l{constructor(i,t,n){this.plugin=i,this.event=t,this.channelId=n}async unregister(){return f(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}}async function u(i,t,n){const e=new s(n);return f(`plugin:${i}|registerListener`,{event:t,handler:e}).then((()=>new l(i,t,e.id)))}async function f(i,t={},n){return window.__TAURI_INTERNALS__.invoke(i,t,n)}var h,d,w;i.ScheduleEvery=void 0,(h=i.ScheduleEvery||(i.ScheduleEvery={})).Year="year",h.Month="month",h.TwoWeeks="twoWeeks",h.Week="week",h.Day="day",h.Hour="hour",h.Minute="minute",h.Second="second";return i.Importance=void 0,(d=i.Importance||(i.Importance={}))[d.None=0]="None",d[d.Min=1]="Min",d[d.Low=2]="Low",d[d.Default=3]="Default",d[d.High=4]="High",i.Visibility=void 0,(w=i.Visibility||(i.Visibility={}))[w.Secret=-1]="Secret",w[w.Private=0]="Private",w[w.Public=1]="Public",i.Schedule=class{static at(i,t=!1,n=!1){return{at:{date:i,repeating:t,allowWhileIdle:n},interval:void 0,every:void 0}}static interval(i,t=!1){return{at:void 0,interval:{interval:i,allowWhileIdle:t},every:void 0}}static every(i,t,n=!1){return{at:void 0,interval:void 0,every:{interval:i,count:t,allowWhileIdle:n}}}},i.active=async function(){return await f("plugin:notification|get_active")},i.cancel=async function(i){await f("plugin:notification|cancel",{notifications:i})},i.cancelAll=async function(){await f("plugin:notification|cancel")},i.channels=async function(){return await f("plugin:notification|listChannels")},i.createChannel=async function(i){await f("plugin:notification|create_channel",{...i})},i.isPermissionGranted=async function(){return"default"!==window.Notification.permission?await Promise.resolve("granted"===window.Notification.permission):await f("plugin:notification|is_permission_granted")},i.onAction=async function(i){return await u("notification","actionPerformed",i)},i.onNotificationReceived=async function(i){return await u("notification","notification",i)},i.pending=async function(){return await f("plugin:notification|get_pending")},i.registerActionTypes=async function(i){await f("plugin:notification|register_action_types",{types:i})},i.removeActive=async function(i){await f("plugin:notification|remove_active",{notifications:i})},i.removeAllActive=async function(){await f("plugin:notification|remove_active")},i.removeChannel=async function(i){await f("plugin:notification|delete_channel",{id:i})},i.requestPermission=async function(){return await window.Notification.requestPermission()},i.sendNotification=function(i){"string"==typeof i?new window.Notification(i):new window.Notification(i.title,i)},i}({});Object.defineProperty(window.__TAURI__,"notification",{value:__TAURI_PLUGIN_NOTIFICATION__})}
2 |
--------------------------------------------------------------------------------
/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tauri-apps/tauri-plugin-notification/665d8f08bcf2e8af3c0f95af12ca1f06d71a0d6d/banner.png
--------------------------------------------------------------------------------
/build.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | const COMMANDS: &[&str] = &[
6 | "notify",
7 | "request_permission",
8 | "is_permission_granted",
9 | "register_action_types",
10 | "register_listener",
11 | "cancel",
12 | "get_pending",
13 | "remove_active",
14 | "get_active",
15 | "check_permissions",
16 | "show",
17 | "batch",
18 | "list_channels",
19 | "delete_channel",
20 | "create_channel",
21 | "permission_state",
22 | ];
23 |
24 | fn main() {
25 | let result = tauri_plugin::Builder::new(COMMANDS)
26 | .global_api_script_path("./api-iife.js")
27 | .android_path("android")
28 | .ios_path("ios")
29 | .try_build();
30 |
31 | // when building documentation for Android the plugin build result is always Err() and is irrelevant to the crate documentation build
32 | if !(cfg!(docsrs) && std::env::var("TARGET").unwrap().contains("android")) {
33 | result.unwrap();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/dist-js/index.cjs:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var core = require('@tauri-apps/api/core');
4 |
5 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
6 | // SPDX-License-Identifier: Apache-2.0
7 | // SPDX-License-Identifier: MIT
8 | /**
9 | * Send toast notifications (brief auto-expiring OS window element) to your user.
10 | * Can also be used with the Notification Web API.
11 | *
12 | * @module
13 | */
14 | exports.ScheduleEvery = void 0;
15 | (function (ScheduleEvery) {
16 | ScheduleEvery["Year"] = "year";
17 | ScheduleEvery["Month"] = "month";
18 | ScheduleEvery["TwoWeeks"] = "twoWeeks";
19 | ScheduleEvery["Week"] = "week";
20 | ScheduleEvery["Day"] = "day";
21 | ScheduleEvery["Hour"] = "hour";
22 | ScheduleEvery["Minute"] = "minute";
23 | /**
24 | * Not supported on iOS.
25 | */
26 | ScheduleEvery["Second"] = "second";
27 | })(exports.ScheduleEvery || (exports.ScheduleEvery = {}));
28 | class Schedule {
29 | static at(date, repeating = false, allowWhileIdle = false) {
30 | return {
31 | at: { date, repeating, allowWhileIdle },
32 | interval: undefined,
33 | every: undefined
34 | };
35 | }
36 | static interval(interval, allowWhileIdle = false) {
37 | return {
38 | at: undefined,
39 | interval: { interval, allowWhileIdle },
40 | every: undefined
41 | };
42 | }
43 | static every(kind, count, allowWhileIdle = false) {
44 | return {
45 | at: undefined,
46 | interval: undefined,
47 | every: { interval: kind, count, allowWhileIdle }
48 | };
49 | }
50 | }
51 | exports.Importance = void 0;
52 | (function (Importance) {
53 | Importance[Importance["None"] = 0] = "None";
54 | Importance[Importance["Min"] = 1] = "Min";
55 | Importance[Importance["Low"] = 2] = "Low";
56 | Importance[Importance["Default"] = 3] = "Default";
57 | Importance[Importance["High"] = 4] = "High";
58 | })(exports.Importance || (exports.Importance = {}));
59 | exports.Visibility = void 0;
60 | (function (Visibility) {
61 | Visibility[Visibility["Secret"] = -1] = "Secret";
62 | Visibility[Visibility["Private"] = 0] = "Private";
63 | Visibility[Visibility["Public"] = 1] = "Public";
64 | })(exports.Visibility || (exports.Visibility = {}));
65 | /**
66 | * Checks if the permission to send notifications is granted.
67 | * @example
68 | * ```typescript
69 | * import { isPermissionGranted } from '@tauri-apps/plugin-notification';
70 | * const permissionGranted = await isPermissionGranted();
71 | * ```
72 | *
73 | * @since 2.0.0
74 | */
75 | async function isPermissionGranted() {
76 | if (window.Notification.permission !== 'default') {
77 | return await Promise.resolve(window.Notification.permission === 'granted');
78 | }
79 | return await core.invoke('plugin:notification|is_permission_granted');
80 | }
81 | /**
82 | * Requests the permission to send notifications.
83 | * @example
84 | * ```typescript
85 | * import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notification';
86 | * let permissionGranted = await isPermissionGranted();
87 | * if (!permissionGranted) {
88 | * const permission = await requestPermission();
89 | * permissionGranted = permission === 'granted';
90 | * }
91 | * ```
92 | *
93 | * @returns A promise resolving to whether the user granted the permission or not.
94 | *
95 | * @since 2.0.0
96 | */
97 | async function requestPermission() {
98 | return await window.Notification.requestPermission();
99 | }
100 | /**
101 | * Sends a notification to the user.
102 | * @example
103 | * ```typescript
104 | * import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/plugin-notification';
105 | * let permissionGranted = await isPermissionGranted();
106 | * if (!permissionGranted) {
107 | * const permission = await requestPermission();
108 | * permissionGranted = permission === 'granted';
109 | * }
110 | * if (permissionGranted) {
111 | * sendNotification('Tauri is awesome!');
112 | * sendNotification({ title: 'TAURI', body: 'Tauri is awesome!' });
113 | * }
114 | * ```
115 | *
116 | * @since 2.0.0
117 | */
118 | function sendNotification(options) {
119 | if (typeof options === 'string') {
120 | new window.Notification(options);
121 | }
122 | else {
123 | new window.Notification(options.title, options);
124 | }
125 | }
126 | /**
127 | * Register actions that are performed when the user clicks on the notification.
128 | *
129 | * @example
130 | * ```typescript
131 | * import { registerActionTypes } from '@tauri-apps/plugin-notification';
132 | * await registerActionTypes([{
133 | * id: 'tauri',
134 | * actions: [{
135 | * id: 'my-action',
136 | * title: 'Settings'
137 | * }]
138 | * }])
139 | * ```
140 | *
141 | * @returns A promise indicating the success or failure of the operation.
142 | *
143 | * @since 2.0.0
144 | */
145 | async function registerActionTypes(types) {
146 | await core.invoke('plugin:notification|register_action_types', { types });
147 | }
148 | /**
149 | * Retrieves the list of pending notifications.
150 | *
151 | * @example
152 | * ```typescript
153 | * import { pending } from '@tauri-apps/plugin-notification';
154 | * const pendingNotifications = await pending();
155 | * ```
156 | *
157 | * @returns A promise resolving to the list of pending notifications.
158 | *
159 | * @since 2.0.0
160 | */
161 | async function pending() {
162 | return await core.invoke('plugin:notification|get_pending');
163 | }
164 | /**
165 | * Cancels the pending notifications with the given list of identifiers.
166 | *
167 | * @example
168 | * ```typescript
169 | * import { cancel } from '@tauri-apps/plugin-notification';
170 | * await cancel([-34234, 23432, 4311]);
171 | * ```
172 | *
173 | * @returns A promise indicating the success or failure of the operation.
174 | *
175 | * @since 2.0.0
176 | */
177 | async function cancel(notifications) {
178 | await core.invoke('plugin:notification|cancel', { notifications });
179 | }
180 | /**
181 | * Cancels all pending notifications.
182 | *
183 | * @example
184 | * ```typescript
185 | * import { cancelAll } from '@tauri-apps/plugin-notification';
186 | * await cancelAll();
187 | * ```
188 | *
189 | * @returns A promise indicating the success or failure of the operation.
190 | *
191 | * @since 2.0.0
192 | */
193 | async function cancelAll() {
194 | await core.invoke('plugin:notification|cancel');
195 | }
196 | /**
197 | * Retrieves the list of active notifications.
198 | *
199 | * @example
200 | * ```typescript
201 | * import { active } from '@tauri-apps/plugin-notification';
202 | * const activeNotifications = await active();
203 | * ```
204 | *
205 | * @returns A promise resolving to the list of active notifications.
206 | *
207 | * @since 2.0.0
208 | */
209 | async function active() {
210 | return await core.invoke('plugin:notification|get_active');
211 | }
212 | /**
213 | * Removes the active notifications with the given list of identifiers.
214 | *
215 | * @example
216 | * ```typescript
217 | * import { cancel } from '@tauri-apps/plugin-notification';
218 | * await cancel([-34234, 23432, 4311])
219 | * ```
220 | *
221 | * @returns A promise indicating the success or failure of the operation.
222 | *
223 | * @since 2.0.0
224 | */
225 | async function removeActive(notifications) {
226 | await core.invoke('plugin:notification|remove_active', { notifications });
227 | }
228 | /**
229 | * Removes all active notifications.
230 | *
231 | * @example
232 | * ```typescript
233 | * import { removeAllActive } from '@tauri-apps/plugin-notification';
234 | * await removeAllActive()
235 | * ```
236 | *
237 | * @returns A promise indicating the success or failure of the operation.
238 | *
239 | * @since 2.0.0
240 | */
241 | async function removeAllActive() {
242 | await core.invoke('plugin:notification|remove_active');
243 | }
244 | /**
245 | * Creates a notification channel.
246 | *
247 | * @example
248 | * ```typescript
249 | * import { createChannel, Importance, Visibility } from '@tauri-apps/plugin-notification';
250 | * await createChannel({
251 | * id: 'new-messages',
252 | * name: 'New Messages',
253 | * lights: true,
254 | * vibration: true,
255 | * importance: Importance.Default,
256 | * visibility: Visibility.Private
257 | * });
258 | * ```
259 | *
260 | * @returns A promise indicating the success or failure of the operation.
261 | *
262 | * @since 2.0.0
263 | */
264 | async function createChannel(channel) {
265 | await core.invoke('plugin:notification|create_channel', { ...channel });
266 | }
267 | /**
268 | * Removes the channel with the given identifier.
269 | *
270 | * @example
271 | * ```typescript
272 | * import { removeChannel } from '@tauri-apps/plugin-notification';
273 | * await removeChannel();
274 | * ```
275 | *
276 | * @returns A promise indicating the success or failure of the operation.
277 | *
278 | * @since 2.0.0
279 | */
280 | async function removeChannel(id) {
281 | await core.invoke('plugin:notification|delete_channel', { id });
282 | }
283 | /**
284 | * Retrieves the list of notification channels.
285 | *
286 | * @example
287 | * ```typescript
288 | * import { channels } from '@tauri-apps/plugin-notification';
289 | * const notificationChannels = await channels();
290 | * ```
291 | *
292 | * @returns A promise resolving to the list of notification channels.
293 | *
294 | * @since 2.0.0
295 | */
296 | async function channels() {
297 | return await core.invoke('plugin:notification|listChannels');
298 | }
299 | async function onNotificationReceived(cb) {
300 | return await core.addPluginListener('notification', 'notification', cb);
301 | }
302 | async function onAction(cb) {
303 | return await core.addPluginListener('notification', 'actionPerformed', cb);
304 | }
305 |
306 | exports.Schedule = Schedule;
307 | exports.active = active;
308 | exports.cancel = cancel;
309 | exports.cancelAll = cancelAll;
310 | exports.channels = channels;
311 | exports.createChannel = createChannel;
312 | exports.isPermissionGranted = isPermissionGranted;
313 | exports.onAction = onAction;
314 | exports.onNotificationReceived = onNotificationReceived;
315 | exports.pending = pending;
316 | exports.registerActionTypes = registerActionTypes;
317 | exports.removeActive = removeActive;
318 | exports.removeAllActive = removeAllActive;
319 | exports.removeChannel = removeChannel;
320 | exports.requestPermission = requestPermission;
321 | exports.sendNotification = sendNotification;
322 |
--------------------------------------------------------------------------------
/dist-js/index.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Send toast notifications (brief auto-expiring OS window element) to your user.
3 | * Can also be used with the Notification Web API.
4 | *
5 | * @module
6 | */
7 | import { type PluginListener } from '@tauri-apps/api/core';
8 | export type { PermissionState } from '@tauri-apps/api/core';
9 | /**
10 | * Options to send a notification.
11 | *
12 | * @since 2.0.0
13 | */
14 | interface Options {
15 | /**
16 | * The notification identifier to reference this object later. Must be a 32-bit integer.
17 | */
18 | id?: number;
19 | /**
20 | * Identifier of the {@link Channel} that deliveres this notification.
21 | *
22 | * If the channel does not exist, the notification won't fire.
23 | * Make sure the channel exists with {@link listChannels} and {@link createChannel}.
24 | */
25 | channelId?: string;
26 | /**
27 | * Notification title.
28 | */
29 | title: string;
30 | /**
31 | * Optional notification body.
32 | * */
33 | body?: string;
34 | /**
35 | * Schedule this notification to fire on a later time or a fixed interval.
36 | */
37 | schedule?: Schedule;
38 | /**
39 | * Multiline text.
40 | * Changes the notification style to big text.
41 | * Cannot be used with `inboxLines`.
42 | */
43 | largeBody?: string;
44 | /**
45 | * Detail text for the notification with `largeBody`, `inboxLines` or `groupSummary`.
46 | */
47 | summary?: string;
48 | /**
49 | * Defines an action type for this notification.
50 | */
51 | actionTypeId?: string;
52 | /**
53 | * Identifier used to group multiple notifications.
54 | *
55 | * https://developer.apple.com/documentation/usernotifications/unmutablenotificationcontent/1649872-threadidentifier
56 | */
57 | group?: string;
58 | /**
59 | * Instructs the system that this notification is the summary of a group on Android.
60 | */
61 | groupSummary?: boolean;
62 | /**
63 | * The sound resource name. Only available on mobile.
64 | */
65 | sound?: string;
66 | /**
67 | * List of lines to add to the notification.
68 | * Changes the notification style to inbox.
69 | * Cannot be used with `largeBody`.
70 | *
71 | * Only supports up to 5 lines.
72 | */
73 | inboxLines?: string[];
74 | /**
75 | * Notification icon.
76 | *
77 | * On Android the icon must be placed in the app's `res/drawable` folder.
78 | */
79 | icon?: string;
80 | /**
81 | * Notification large icon (Android).
82 | *
83 | * The icon must be placed in the app's `res/drawable` folder.
84 | */
85 | largeIcon?: string;
86 | /**
87 | * Icon color on Android.
88 | */
89 | iconColor?: string;
90 | /**
91 | * Notification attachments.
92 | */
93 | attachments?: Attachment[];
94 | /**
95 | * Extra payload to store in the notification.
96 | */
97 | extra?: Record;
98 | /**
99 | * If true, the notification cannot be dismissed by the user on Android.
100 | *
101 | * An application service must manage the dismissal of the notification.
102 | * It is typically used to indicate a background task that is pending (e.g. a file download)
103 | * or the user is engaged with (e.g. playing music).
104 | */
105 | ongoing?: boolean;
106 | /**
107 | * Automatically cancel the notification when the user clicks on it.
108 | */
109 | autoCancel?: boolean;
110 | /**
111 | * Changes the notification presentation to be silent on iOS (no badge, no sound, not listed).
112 | */
113 | silent?: boolean;
114 | /**
115 | * Notification visibility.
116 | */
117 | visibility?: Visibility;
118 | /**
119 | * Sets the number of items this notification represents on Android.
120 | */
121 | number?: number;
122 | }
123 | interface ScheduleInterval {
124 | year?: number;
125 | month?: number;
126 | day?: number;
127 | /**
128 | * 1 - Sunday
129 | * 2 - Monday
130 | * 3 - Tuesday
131 | * 4 - Wednesday
132 | * 5 - Thursday
133 | * 6 - Friday
134 | * 7 - Saturday
135 | */
136 | weekday?: number;
137 | hour?: number;
138 | minute?: number;
139 | second?: number;
140 | }
141 | declare enum ScheduleEvery {
142 | Year = "year",
143 | Month = "month",
144 | TwoWeeks = "twoWeeks",
145 | Week = "week",
146 | Day = "day",
147 | Hour = "hour",
148 | Minute = "minute",
149 | /**
150 | * Not supported on iOS.
151 | */
152 | Second = "second"
153 | }
154 | declare class Schedule {
155 | at: {
156 | date: Date;
157 | repeating: boolean;
158 | allowWhileIdle: boolean;
159 | } | undefined;
160 | interval: {
161 | interval: ScheduleInterval;
162 | allowWhileIdle: boolean;
163 | } | undefined;
164 | every: {
165 | interval: ScheduleEvery;
166 | count: number;
167 | allowWhileIdle: boolean;
168 | } | undefined;
169 | static at(date: Date, repeating?: boolean, allowWhileIdle?: boolean): Schedule;
170 | static interval(interval: ScheduleInterval, allowWhileIdle?: boolean): Schedule;
171 | static every(kind: ScheduleEvery, count: number, allowWhileIdle?: boolean): Schedule;
172 | }
173 | /**
174 | * Attachment of a notification.
175 | */
176 | interface Attachment {
177 | /** Attachment identifier. */
178 | id: string;
179 | /** Attachment URL. Accepts the `asset` and `file` protocols. */
180 | url: string;
181 | }
182 | interface Action {
183 | id: string;
184 | title: string;
185 | requiresAuthentication?: boolean;
186 | foreground?: boolean;
187 | destructive?: boolean;
188 | input?: boolean;
189 | inputButtonTitle?: string;
190 | inputPlaceholder?: string;
191 | }
192 | interface ActionType {
193 | /**
194 | * The identifier of this action type
195 | */
196 | id: string;
197 | /**
198 | * The list of associated actions
199 | */
200 | actions: Action[];
201 | hiddenPreviewsBodyPlaceholder?: string;
202 | customDismissAction?: boolean;
203 | allowInCarPlay?: boolean;
204 | hiddenPreviewsShowTitle?: boolean;
205 | hiddenPreviewsShowSubtitle?: boolean;
206 | }
207 | interface PendingNotification {
208 | id: number;
209 | title?: string;
210 | body?: string;
211 | schedule: Schedule;
212 | }
213 | interface ActiveNotification {
214 | id: number;
215 | tag?: string;
216 | title?: string;
217 | body?: string;
218 | group?: string;
219 | groupSummary: boolean;
220 | data: Record;
221 | extra: Record;
222 | attachments: Attachment[];
223 | actionTypeId?: string;
224 | schedule?: Schedule;
225 | sound?: string;
226 | }
227 | declare enum Importance {
228 | None = 0,
229 | Min = 1,
230 | Low = 2,
231 | Default = 3,
232 | High = 4
233 | }
234 | declare enum Visibility {
235 | Secret = -1,
236 | Private = 0,
237 | Public = 1
238 | }
239 | interface Channel {
240 | id: string;
241 | name: string;
242 | description?: string;
243 | sound?: string;
244 | lights?: boolean;
245 | lightColor?: string;
246 | vibration?: boolean;
247 | importance?: Importance;
248 | visibility?: Visibility;
249 | }
250 | /**
251 | * Checks if the permission to send notifications is granted.
252 | * @example
253 | * ```typescript
254 | * import { isPermissionGranted } from '@tauri-apps/plugin-notification';
255 | * const permissionGranted = await isPermissionGranted();
256 | * ```
257 | *
258 | * @since 2.0.0
259 | */
260 | declare function isPermissionGranted(): Promise;
261 | /**
262 | * Requests the permission to send notifications.
263 | * @example
264 | * ```typescript
265 | * import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notification';
266 | * let permissionGranted = await isPermissionGranted();
267 | * if (!permissionGranted) {
268 | * const permission = await requestPermission();
269 | * permissionGranted = permission === 'granted';
270 | * }
271 | * ```
272 | *
273 | * @returns A promise resolving to whether the user granted the permission or not.
274 | *
275 | * @since 2.0.0
276 | */
277 | declare function requestPermission(): Promise;
278 | /**
279 | * Sends a notification to the user.
280 | * @example
281 | * ```typescript
282 | * import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/plugin-notification';
283 | * let permissionGranted = await isPermissionGranted();
284 | * if (!permissionGranted) {
285 | * const permission = await requestPermission();
286 | * permissionGranted = permission === 'granted';
287 | * }
288 | * if (permissionGranted) {
289 | * sendNotification('Tauri is awesome!');
290 | * sendNotification({ title: 'TAURI', body: 'Tauri is awesome!' });
291 | * }
292 | * ```
293 | *
294 | * @since 2.0.0
295 | */
296 | declare function sendNotification(options: Options | string): void;
297 | /**
298 | * Register actions that are performed when the user clicks on the notification.
299 | *
300 | * @example
301 | * ```typescript
302 | * import { registerActionTypes } from '@tauri-apps/plugin-notification';
303 | * await registerActionTypes([{
304 | * id: 'tauri',
305 | * actions: [{
306 | * id: 'my-action',
307 | * title: 'Settings'
308 | * }]
309 | * }])
310 | * ```
311 | *
312 | * @returns A promise indicating the success or failure of the operation.
313 | *
314 | * @since 2.0.0
315 | */
316 | declare function registerActionTypes(types: ActionType[]): Promise;
317 | /**
318 | * Retrieves the list of pending notifications.
319 | *
320 | * @example
321 | * ```typescript
322 | * import { pending } from '@tauri-apps/plugin-notification';
323 | * const pendingNotifications = await pending();
324 | * ```
325 | *
326 | * @returns A promise resolving to the list of pending notifications.
327 | *
328 | * @since 2.0.0
329 | */
330 | declare function pending(): Promise;
331 | /**
332 | * Cancels the pending notifications with the given list of identifiers.
333 | *
334 | * @example
335 | * ```typescript
336 | * import { cancel } from '@tauri-apps/plugin-notification';
337 | * await cancel([-34234, 23432, 4311]);
338 | * ```
339 | *
340 | * @returns A promise indicating the success or failure of the operation.
341 | *
342 | * @since 2.0.0
343 | */
344 | declare function cancel(notifications: number[]): Promise;
345 | /**
346 | * Cancels all pending notifications.
347 | *
348 | * @example
349 | * ```typescript
350 | * import { cancelAll } from '@tauri-apps/plugin-notification';
351 | * await cancelAll();
352 | * ```
353 | *
354 | * @returns A promise indicating the success or failure of the operation.
355 | *
356 | * @since 2.0.0
357 | */
358 | declare function cancelAll(): Promise;
359 | /**
360 | * Retrieves the list of active notifications.
361 | *
362 | * @example
363 | * ```typescript
364 | * import { active } from '@tauri-apps/plugin-notification';
365 | * const activeNotifications = await active();
366 | * ```
367 | *
368 | * @returns A promise resolving to the list of active notifications.
369 | *
370 | * @since 2.0.0
371 | */
372 | declare function active(): Promise;
373 | /**
374 | * Removes the active notifications with the given list of identifiers.
375 | *
376 | * @example
377 | * ```typescript
378 | * import { cancel } from '@tauri-apps/plugin-notification';
379 | * await cancel([-34234, 23432, 4311])
380 | * ```
381 | *
382 | * @returns A promise indicating the success or failure of the operation.
383 | *
384 | * @since 2.0.0
385 | */
386 | declare function removeActive(notifications: Array<{
387 | id: number;
388 | tag?: string;
389 | }>): Promise;
390 | /**
391 | * Removes all active notifications.
392 | *
393 | * @example
394 | * ```typescript
395 | * import { removeAllActive } from '@tauri-apps/plugin-notification';
396 | * await removeAllActive()
397 | * ```
398 | *
399 | * @returns A promise indicating the success or failure of the operation.
400 | *
401 | * @since 2.0.0
402 | */
403 | declare function removeAllActive(): Promise;
404 | /**
405 | * Creates a notification channel.
406 | *
407 | * @example
408 | * ```typescript
409 | * import { createChannel, Importance, Visibility } from '@tauri-apps/plugin-notification';
410 | * await createChannel({
411 | * id: 'new-messages',
412 | * name: 'New Messages',
413 | * lights: true,
414 | * vibration: true,
415 | * importance: Importance.Default,
416 | * visibility: Visibility.Private
417 | * });
418 | * ```
419 | *
420 | * @returns A promise indicating the success or failure of the operation.
421 | *
422 | * @since 2.0.0
423 | */
424 | declare function createChannel(channel: Channel): Promise;
425 | /**
426 | * Removes the channel with the given identifier.
427 | *
428 | * @example
429 | * ```typescript
430 | * import { removeChannel } from '@tauri-apps/plugin-notification';
431 | * await removeChannel();
432 | * ```
433 | *
434 | * @returns A promise indicating the success or failure of the operation.
435 | *
436 | * @since 2.0.0
437 | */
438 | declare function removeChannel(id: string): Promise;
439 | /**
440 | * Retrieves the list of notification channels.
441 | *
442 | * @example
443 | * ```typescript
444 | * import { channels } from '@tauri-apps/plugin-notification';
445 | * const notificationChannels = await channels();
446 | * ```
447 | *
448 | * @returns A promise resolving to the list of notification channels.
449 | *
450 | * @since 2.0.0
451 | */
452 | declare function channels(): Promise;
453 | declare function onNotificationReceived(cb: (notification: Options) => void): Promise;
454 | declare function onAction(cb: (notification: Options) => void): Promise;
455 | export type { Attachment, Options, Action, ActionType, PendingNotification, ActiveNotification, Channel, ScheduleInterval };
456 | export { Importance, Visibility, sendNotification, requestPermission, isPermissionGranted, registerActionTypes, pending, cancel, cancelAll, active, removeActive, removeAllActive, createChannel, removeChannel, channels, onNotificationReceived, onAction, Schedule, ScheduleEvery };
457 |
--------------------------------------------------------------------------------
/dist-js/index.js:
--------------------------------------------------------------------------------
1 | import { invoke, addPluginListener } from '@tauri-apps/api/core';
2 |
3 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
4 | // SPDX-License-Identifier: Apache-2.0
5 | // SPDX-License-Identifier: MIT
6 | /**
7 | * Send toast notifications (brief auto-expiring OS window element) to your user.
8 | * Can also be used with the Notification Web API.
9 | *
10 | * @module
11 | */
12 | var ScheduleEvery;
13 | (function (ScheduleEvery) {
14 | ScheduleEvery["Year"] = "year";
15 | ScheduleEvery["Month"] = "month";
16 | ScheduleEvery["TwoWeeks"] = "twoWeeks";
17 | ScheduleEvery["Week"] = "week";
18 | ScheduleEvery["Day"] = "day";
19 | ScheduleEvery["Hour"] = "hour";
20 | ScheduleEvery["Minute"] = "minute";
21 | /**
22 | * Not supported on iOS.
23 | */
24 | ScheduleEvery["Second"] = "second";
25 | })(ScheduleEvery || (ScheduleEvery = {}));
26 | class Schedule {
27 | static at(date, repeating = false, allowWhileIdle = false) {
28 | return {
29 | at: { date, repeating, allowWhileIdle },
30 | interval: undefined,
31 | every: undefined
32 | };
33 | }
34 | static interval(interval, allowWhileIdle = false) {
35 | return {
36 | at: undefined,
37 | interval: { interval, allowWhileIdle },
38 | every: undefined
39 | };
40 | }
41 | static every(kind, count, allowWhileIdle = false) {
42 | return {
43 | at: undefined,
44 | interval: undefined,
45 | every: { interval: kind, count, allowWhileIdle }
46 | };
47 | }
48 | }
49 | var Importance;
50 | (function (Importance) {
51 | Importance[Importance["None"] = 0] = "None";
52 | Importance[Importance["Min"] = 1] = "Min";
53 | Importance[Importance["Low"] = 2] = "Low";
54 | Importance[Importance["Default"] = 3] = "Default";
55 | Importance[Importance["High"] = 4] = "High";
56 | })(Importance || (Importance = {}));
57 | var Visibility;
58 | (function (Visibility) {
59 | Visibility[Visibility["Secret"] = -1] = "Secret";
60 | Visibility[Visibility["Private"] = 0] = "Private";
61 | Visibility[Visibility["Public"] = 1] = "Public";
62 | })(Visibility || (Visibility = {}));
63 | /**
64 | * Checks if the permission to send notifications is granted.
65 | * @example
66 | * ```typescript
67 | * import { isPermissionGranted } from '@tauri-apps/plugin-notification';
68 | * const permissionGranted = await isPermissionGranted();
69 | * ```
70 | *
71 | * @since 2.0.0
72 | */
73 | async function isPermissionGranted() {
74 | if (window.Notification.permission !== 'default') {
75 | return await Promise.resolve(window.Notification.permission === 'granted');
76 | }
77 | return await invoke('plugin:notification|is_permission_granted');
78 | }
79 | /**
80 | * Requests the permission to send notifications.
81 | * @example
82 | * ```typescript
83 | * import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notification';
84 | * let permissionGranted = await isPermissionGranted();
85 | * if (!permissionGranted) {
86 | * const permission = await requestPermission();
87 | * permissionGranted = permission === 'granted';
88 | * }
89 | * ```
90 | *
91 | * @returns A promise resolving to whether the user granted the permission or not.
92 | *
93 | * @since 2.0.0
94 | */
95 | async function requestPermission() {
96 | return await window.Notification.requestPermission();
97 | }
98 | /**
99 | * Sends a notification to the user.
100 | * @example
101 | * ```typescript
102 | * import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/plugin-notification';
103 | * let permissionGranted = await isPermissionGranted();
104 | * if (!permissionGranted) {
105 | * const permission = await requestPermission();
106 | * permissionGranted = permission === 'granted';
107 | * }
108 | * if (permissionGranted) {
109 | * sendNotification('Tauri is awesome!');
110 | * sendNotification({ title: 'TAURI', body: 'Tauri is awesome!' });
111 | * }
112 | * ```
113 | *
114 | * @since 2.0.0
115 | */
116 | function sendNotification(options) {
117 | if (typeof options === 'string') {
118 | new window.Notification(options);
119 | }
120 | else {
121 | new window.Notification(options.title, options);
122 | }
123 | }
124 | /**
125 | * Register actions that are performed when the user clicks on the notification.
126 | *
127 | * @example
128 | * ```typescript
129 | * import { registerActionTypes } from '@tauri-apps/plugin-notification';
130 | * await registerActionTypes([{
131 | * id: 'tauri',
132 | * actions: [{
133 | * id: 'my-action',
134 | * title: 'Settings'
135 | * }]
136 | * }])
137 | * ```
138 | *
139 | * @returns A promise indicating the success or failure of the operation.
140 | *
141 | * @since 2.0.0
142 | */
143 | async function registerActionTypes(types) {
144 | await invoke('plugin:notification|register_action_types', { types });
145 | }
146 | /**
147 | * Retrieves the list of pending notifications.
148 | *
149 | * @example
150 | * ```typescript
151 | * import { pending } from '@tauri-apps/plugin-notification';
152 | * const pendingNotifications = await pending();
153 | * ```
154 | *
155 | * @returns A promise resolving to the list of pending notifications.
156 | *
157 | * @since 2.0.0
158 | */
159 | async function pending() {
160 | return await invoke('plugin:notification|get_pending');
161 | }
162 | /**
163 | * Cancels the pending notifications with the given list of identifiers.
164 | *
165 | * @example
166 | * ```typescript
167 | * import { cancel } from '@tauri-apps/plugin-notification';
168 | * await cancel([-34234, 23432, 4311]);
169 | * ```
170 | *
171 | * @returns A promise indicating the success or failure of the operation.
172 | *
173 | * @since 2.0.0
174 | */
175 | async function cancel(notifications) {
176 | await invoke('plugin:notification|cancel', { notifications });
177 | }
178 | /**
179 | * Cancels all pending notifications.
180 | *
181 | * @example
182 | * ```typescript
183 | * import { cancelAll } from '@tauri-apps/plugin-notification';
184 | * await cancelAll();
185 | * ```
186 | *
187 | * @returns A promise indicating the success or failure of the operation.
188 | *
189 | * @since 2.0.0
190 | */
191 | async function cancelAll() {
192 | await invoke('plugin:notification|cancel');
193 | }
194 | /**
195 | * Retrieves the list of active notifications.
196 | *
197 | * @example
198 | * ```typescript
199 | * import { active } from '@tauri-apps/plugin-notification';
200 | * const activeNotifications = await active();
201 | * ```
202 | *
203 | * @returns A promise resolving to the list of active notifications.
204 | *
205 | * @since 2.0.0
206 | */
207 | async function active() {
208 | return await invoke('plugin:notification|get_active');
209 | }
210 | /**
211 | * Removes the active notifications with the given list of identifiers.
212 | *
213 | * @example
214 | * ```typescript
215 | * import { cancel } from '@tauri-apps/plugin-notification';
216 | * await cancel([-34234, 23432, 4311])
217 | * ```
218 | *
219 | * @returns A promise indicating the success or failure of the operation.
220 | *
221 | * @since 2.0.0
222 | */
223 | async function removeActive(notifications) {
224 | await invoke('plugin:notification|remove_active', { notifications });
225 | }
226 | /**
227 | * Removes all active notifications.
228 | *
229 | * @example
230 | * ```typescript
231 | * import { removeAllActive } from '@tauri-apps/plugin-notification';
232 | * await removeAllActive()
233 | * ```
234 | *
235 | * @returns A promise indicating the success or failure of the operation.
236 | *
237 | * @since 2.0.0
238 | */
239 | async function removeAllActive() {
240 | await invoke('plugin:notification|remove_active');
241 | }
242 | /**
243 | * Creates a notification channel.
244 | *
245 | * @example
246 | * ```typescript
247 | * import { createChannel, Importance, Visibility } from '@tauri-apps/plugin-notification';
248 | * await createChannel({
249 | * id: 'new-messages',
250 | * name: 'New Messages',
251 | * lights: true,
252 | * vibration: true,
253 | * importance: Importance.Default,
254 | * visibility: Visibility.Private
255 | * });
256 | * ```
257 | *
258 | * @returns A promise indicating the success or failure of the operation.
259 | *
260 | * @since 2.0.0
261 | */
262 | async function createChannel(channel) {
263 | await invoke('plugin:notification|create_channel', { ...channel });
264 | }
265 | /**
266 | * Removes the channel with the given identifier.
267 | *
268 | * @example
269 | * ```typescript
270 | * import { removeChannel } from '@tauri-apps/plugin-notification';
271 | * await removeChannel();
272 | * ```
273 | *
274 | * @returns A promise indicating the success or failure of the operation.
275 | *
276 | * @since 2.0.0
277 | */
278 | async function removeChannel(id) {
279 | await invoke('plugin:notification|delete_channel', { id });
280 | }
281 | /**
282 | * Retrieves the list of notification channels.
283 | *
284 | * @example
285 | * ```typescript
286 | * import { channels } from '@tauri-apps/plugin-notification';
287 | * const notificationChannels = await channels();
288 | * ```
289 | *
290 | * @returns A promise resolving to the list of notification channels.
291 | *
292 | * @since 2.0.0
293 | */
294 | async function channels() {
295 | return await invoke('plugin:notification|listChannels');
296 | }
297 | async function onNotificationReceived(cb) {
298 | return await addPluginListener('notification', 'notification', cb);
299 | }
300 | async function onAction(cb) {
301 | return await addPluginListener('notification', 'actionPerformed', cb);
302 | }
303 |
304 | export { Importance, Schedule, ScheduleEvery, Visibility, active, cancel, cancelAll, channels, createChannel, isPermissionGranted, onAction, onNotificationReceived, pending, registerActionTypes, removeActive, removeAllActive, removeChannel, requestPermission, sendNotification };
305 |
--------------------------------------------------------------------------------
/dist-js/init.d.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
--------------------------------------------------------------------------------
/guest-js/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | /**
6 | * Send toast notifications (brief auto-expiring OS window element) to your user.
7 | * Can also be used with the Notification Web API.
8 | *
9 | * @module
10 | */
11 |
12 | import {
13 | invoke,
14 | type PluginListener,
15 | addPluginListener
16 | } from '@tauri-apps/api/core'
17 |
18 | export type { PermissionState } from '@tauri-apps/api/core'
19 |
20 | /**
21 | * Options to send a notification.
22 | *
23 | * @since 2.0.0
24 | */
25 | interface Options {
26 | /**
27 | * The notification identifier to reference this object later. Must be a 32-bit integer.
28 | */
29 | id?: number
30 | /**
31 | * Identifier of the {@link Channel} that deliveres this notification.
32 | *
33 | * If the channel does not exist, the notification won't fire.
34 | * Make sure the channel exists with {@link listChannels} and {@link createChannel}.
35 | */
36 | channelId?: string
37 | /**
38 | * Notification title.
39 | */
40 | title: string
41 | /**
42 | * Optional notification body.
43 | * */
44 | body?: string
45 | /**
46 | * Schedule this notification to fire on a later time or a fixed interval.
47 | */
48 | schedule?: Schedule
49 | /**
50 | * Multiline text.
51 | * Changes the notification style to big text.
52 | * Cannot be used with `inboxLines`.
53 | */
54 | largeBody?: string
55 | /**
56 | * Detail text for the notification with `largeBody`, `inboxLines` or `groupSummary`.
57 | */
58 | summary?: string
59 | /**
60 | * Defines an action type for this notification.
61 | */
62 | actionTypeId?: string
63 | /**
64 | * Identifier used to group multiple notifications.
65 | *
66 | * https://developer.apple.com/documentation/usernotifications/unmutablenotificationcontent/1649872-threadidentifier
67 | */
68 | group?: string
69 | /**
70 | * Instructs the system that this notification is the summary of a group on Android.
71 | */
72 | groupSummary?: boolean
73 | /**
74 | * The sound resource name. Only available on mobile.
75 | */
76 | sound?: string
77 | /**
78 | * List of lines to add to the notification.
79 | * Changes the notification style to inbox.
80 | * Cannot be used with `largeBody`.
81 | *
82 | * Only supports up to 5 lines.
83 | */
84 | inboxLines?: string[]
85 | /**
86 | * Notification icon.
87 | *
88 | * On Android the icon must be placed in the app's `res/drawable` folder.
89 | */
90 | icon?: string
91 | /**
92 | * Notification large icon (Android).
93 | *
94 | * The icon must be placed in the app's `res/drawable` folder.
95 | */
96 | largeIcon?: string
97 | /**
98 | * Icon color on Android.
99 | */
100 | iconColor?: string
101 | /**
102 | * Notification attachments.
103 | */
104 | attachments?: Attachment[]
105 | /**
106 | * Extra payload to store in the notification.
107 | */
108 | extra?: Record
109 | /**
110 | * If true, the notification cannot be dismissed by the user on Android.
111 | *
112 | * An application service must manage the dismissal of the notification.
113 | * It is typically used to indicate a background task that is pending (e.g. a file download)
114 | * or the user is engaged with (e.g. playing music).
115 | */
116 | ongoing?: boolean
117 | /**
118 | * Automatically cancel the notification when the user clicks on it.
119 | */
120 | autoCancel?: boolean
121 | /**
122 | * Changes the notification presentation to be silent on iOS (no badge, no sound, not listed).
123 | */
124 | silent?: boolean
125 | /**
126 | * Notification visibility.
127 | */
128 | visibility?: Visibility
129 | /**
130 | * Sets the number of items this notification represents on Android.
131 | */
132 | number?: number
133 | }
134 |
135 | interface ScheduleInterval {
136 | year?: number
137 | month?: number
138 | day?: number
139 | /**
140 | * 1 - Sunday
141 | * 2 - Monday
142 | * 3 - Tuesday
143 | * 4 - Wednesday
144 | * 5 - Thursday
145 | * 6 - Friday
146 | * 7 - Saturday
147 | */
148 | weekday?: number
149 | hour?: number
150 | minute?: number
151 | second?: number
152 | }
153 |
154 | enum ScheduleEvery {
155 | Year = 'year',
156 | Month = 'month',
157 | TwoWeeks = 'twoWeeks',
158 | Week = 'week',
159 | Day = 'day',
160 | Hour = 'hour',
161 | Minute = 'minute',
162 | /**
163 | * Not supported on iOS.
164 | */
165 | Second = 'second'
166 | }
167 |
168 | class Schedule {
169 | at:
170 | | {
171 | date: Date
172 | repeating: boolean
173 | allowWhileIdle: boolean
174 | }
175 | | undefined
176 |
177 | interval:
178 | | {
179 | interval: ScheduleInterval
180 | allowWhileIdle: boolean
181 | }
182 | | undefined
183 |
184 | every:
185 | | {
186 | interval: ScheduleEvery
187 | count: number
188 | allowWhileIdle: boolean
189 | }
190 | | undefined
191 |
192 | static at(date: Date, repeating = false, allowWhileIdle = false): Schedule {
193 | return {
194 | at: { date, repeating, allowWhileIdle },
195 | interval: undefined,
196 | every: undefined
197 | }
198 | }
199 |
200 | static interval(
201 | interval: ScheduleInterval,
202 | allowWhileIdle = false
203 | ): Schedule {
204 | return {
205 | at: undefined,
206 | interval: { interval, allowWhileIdle },
207 | every: undefined
208 | }
209 | }
210 |
211 | static every(
212 | kind: ScheduleEvery,
213 | count: number,
214 | allowWhileIdle = false
215 | ): Schedule {
216 | return {
217 | at: undefined,
218 | interval: undefined,
219 | every: { interval: kind, count, allowWhileIdle }
220 | }
221 | }
222 | }
223 |
224 | /**
225 | * Attachment of a notification.
226 | */
227 | interface Attachment {
228 | /** Attachment identifier. */
229 | id: string
230 | /** Attachment URL. Accepts the `asset` and `file` protocols. */
231 | url: string
232 | }
233 |
234 | interface Action {
235 | id: string
236 | title: string
237 | requiresAuthentication?: boolean
238 | foreground?: boolean
239 | destructive?: boolean
240 | input?: boolean
241 | inputButtonTitle?: string
242 | inputPlaceholder?: string
243 | }
244 |
245 | interface ActionType {
246 | /**
247 | * The identifier of this action type
248 | */
249 | id: string
250 | /**
251 | * The list of associated actions
252 | */
253 | actions: Action[]
254 | hiddenPreviewsBodyPlaceholder?: string
255 | customDismissAction?: boolean
256 | allowInCarPlay?: boolean
257 | hiddenPreviewsShowTitle?: boolean
258 | hiddenPreviewsShowSubtitle?: boolean
259 | }
260 |
261 | interface PendingNotification {
262 | id: number
263 | title?: string
264 | body?: string
265 | schedule: Schedule
266 | }
267 |
268 | interface ActiveNotification {
269 | id: number
270 | tag?: string
271 | title?: string
272 | body?: string
273 | group?: string
274 | groupSummary: boolean
275 | data: Record
276 | extra: Record
277 | attachments: Attachment[]
278 | actionTypeId?: string
279 | schedule?: Schedule
280 | sound?: string
281 | }
282 |
283 | enum Importance {
284 | None = 0,
285 | Min,
286 | Low,
287 | Default,
288 | High
289 | }
290 |
291 | enum Visibility {
292 | Secret = -1,
293 | Private,
294 | Public
295 | }
296 |
297 | interface Channel {
298 | id: string
299 | name: string
300 | description?: string
301 | sound?: string
302 | lights?: boolean
303 | lightColor?: string
304 | vibration?: boolean
305 | importance?: Importance
306 | visibility?: Visibility
307 | }
308 |
309 | /**
310 | * Checks if the permission to send notifications is granted.
311 | * @example
312 | * ```typescript
313 | * import { isPermissionGranted } from '@tauri-apps/plugin-notification';
314 | * const permissionGranted = await isPermissionGranted();
315 | * ```
316 | *
317 | * @since 2.0.0
318 | */
319 | async function isPermissionGranted(): Promise {
320 | if (window.Notification.permission !== 'default') {
321 | return await Promise.resolve(window.Notification.permission === 'granted')
322 | }
323 | return await invoke('plugin:notification|is_permission_granted')
324 | }
325 |
326 | /**
327 | * Requests the permission to send notifications.
328 | * @example
329 | * ```typescript
330 | * import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notification';
331 | * let permissionGranted = await isPermissionGranted();
332 | * if (!permissionGranted) {
333 | * const permission = await requestPermission();
334 | * permissionGranted = permission === 'granted';
335 | * }
336 | * ```
337 | *
338 | * @returns A promise resolving to whether the user granted the permission or not.
339 | *
340 | * @since 2.0.0
341 | */
342 | async function requestPermission(): Promise {
343 | return await window.Notification.requestPermission()
344 | }
345 |
346 | /**
347 | * Sends a notification to the user.
348 | * @example
349 | * ```typescript
350 | * import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/plugin-notification';
351 | * let permissionGranted = await isPermissionGranted();
352 | * if (!permissionGranted) {
353 | * const permission = await requestPermission();
354 | * permissionGranted = permission === 'granted';
355 | * }
356 | * if (permissionGranted) {
357 | * sendNotification('Tauri is awesome!');
358 | * sendNotification({ title: 'TAURI', body: 'Tauri is awesome!' });
359 | * }
360 | * ```
361 | *
362 | * @since 2.0.0
363 | */
364 | function sendNotification(options: Options | string): void {
365 | if (typeof options === 'string') {
366 | new window.Notification(options)
367 | } else {
368 | new window.Notification(options.title, options)
369 | }
370 | }
371 |
372 | /**
373 | * Register actions that are performed when the user clicks on the notification.
374 | *
375 | * @example
376 | * ```typescript
377 | * import { registerActionTypes } from '@tauri-apps/plugin-notification';
378 | * await registerActionTypes([{
379 | * id: 'tauri',
380 | * actions: [{
381 | * id: 'my-action',
382 | * title: 'Settings'
383 | * }]
384 | * }])
385 | * ```
386 | *
387 | * @returns A promise indicating the success or failure of the operation.
388 | *
389 | * @since 2.0.0
390 | */
391 | async function registerActionTypes(types: ActionType[]): Promise {
392 | await invoke('plugin:notification|register_action_types', { types })
393 | }
394 |
395 | /**
396 | * Retrieves the list of pending notifications.
397 | *
398 | * @example
399 | * ```typescript
400 | * import { pending } from '@tauri-apps/plugin-notification';
401 | * const pendingNotifications = await pending();
402 | * ```
403 | *
404 | * @returns A promise resolving to the list of pending notifications.
405 | *
406 | * @since 2.0.0
407 | */
408 | async function pending(): Promise {
409 | return await invoke('plugin:notification|get_pending')
410 | }
411 |
412 | /**
413 | * Cancels the pending notifications with the given list of identifiers.
414 | *
415 | * @example
416 | * ```typescript
417 | * import { cancel } from '@tauri-apps/plugin-notification';
418 | * await cancel([-34234, 23432, 4311]);
419 | * ```
420 | *
421 | * @returns A promise indicating the success or failure of the operation.
422 | *
423 | * @since 2.0.0
424 | */
425 | async function cancel(notifications: number[]): Promise {
426 | await invoke('plugin:notification|cancel', { notifications })
427 | }
428 |
429 | /**
430 | * Cancels all pending notifications.
431 | *
432 | * @example
433 | * ```typescript
434 | * import { cancelAll } from '@tauri-apps/plugin-notification';
435 | * await cancelAll();
436 | * ```
437 | *
438 | * @returns A promise indicating the success or failure of the operation.
439 | *
440 | * @since 2.0.0
441 | */
442 | async function cancelAll(): Promise {
443 | await invoke('plugin:notification|cancel')
444 | }
445 |
446 | /**
447 | * Retrieves the list of active notifications.
448 | *
449 | * @example
450 | * ```typescript
451 | * import { active } from '@tauri-apps/plugin-notification';
452 | * const activeNotifications = await active();
453 | * ```
454 | *
455 | * @returns A promise resolving to the list of active notifications.
456 | *
457 | * @since 2.0.0
458 | */
459 | async function active(): Promise {
460 | return await invoke('plugin:notification|get_active')
461 | }
462 |
463 | /**
464 | * Removes the active notifications with the given list of identifiers.
465 | *
466 | * @example
467 | * ```typescript
468 | * import { cancel } from '@tauri-apps/plugin-notification';
469 | * await cancel([-34234, 23432, 4311])
470 | * ```
471 | *
472 | * @returns A promise indicating the success or failure of the operation.
473 | *
474 | * @since 2.0.0
475 | */
476 | async function removeActive(
477 | notifications: Array<{ id: number; tag?: string }>
478 | ): Promise {
479 | await invoke('plugin:notification|remove_active', { notifications })
480 | }
481 |
482 | /**
483 | * Removes all active notifications.
484 | *
485 | * @example
486 | * ```typescript
487 | * import { removeAllActive } from '@tauri-apps/plugin-notification';
488 | * await removeAllActive()
489 | * ```
490 | *
491 | * @returns A promise indicating the success or failure of the operation.
492 | *
493 | * @since 2.0.0
494 | */
495 | async function removeAllActive(): Promise {
496 | await invoke('plugin:notification|remove_active')
497 | }
498 |
499 | /**
500 | * Creates a notification channel.
501 | *
502 | * @example
503 | * ```typescript
504 | * import { createChannel, Importance, Visibility } from '@tauri-apps/plugin-notification';
505 | * await createChannel({
506 | * id: 'new-messages',
507 | * name: 'New Messages',
508 | * lights: true,
509 | * vibration: true,
510 | * importance: Importance.Default,
511 | * visibility: Visibility.Private
512 | * });
513 | * ```
514 | *
515 | * @returns A promise indicating the success or failure of the operation.
516 | *
517 | * @since 2.0.0
518 | */
519 | async function createChannel(channel: Channel): Promise {
520 | await invoke('plugin:notification|create_channel', { ...channel })
521 | }
522 |
523 | /**
524 | * Removes the channel with the given identifier.
525 | *
526 | * @example
527 | * ```typescript
528 | * import { removeChannel } from '@tauri-apps/plugin-notification';
529 | * await removeChannel();
530 | * ```
531 | *
532 | * @returns A promise indicating the success or failure of the operation.
533 | *
534 | * @since 2.0.0
535 | */
536 | async function removeChannel(id: string): Promise {
537 | await invoke('plugin:notification|delete_channel', { id })
538 | }
539 |
540 | /**
541 | * Retrieves the list of notification channels.
542 | *
543 | * @example
544 | * ```typescript
545 | * import { channels } from '@tauri-apps/plugin-notification';
546 | * const notificationChannels = await channels();
547 | * ```
548 | *
549 | * @returns A promise resolving to the list of notification channels.
550 | *
551 | * @since 2.0.0
552 | */
553 | async function channels(): Promise {
554 | return await invoke('plugin:notification|listChannels')
555 | }
556 |
557 | async function onNotificationReceived(
558 | cb: (notification: Options) => void
559 | ): Promise {
560 | return await addPluginListener('notification', 'notification', cb)
561 | }
562 |
563 | async function onAction(
564 | cb: (notification: Options) => void
565 | ): Promise {
566 | return await addPluginListener('notification', 'actionPerformed', cb)
567 | }
568 |
569 | export type {
570 | Attachment,
571 | Options,
572 | Action,
573 | ActionType,
574 | PendingNotification,
575 | ActiveNotification,
576 | Channel,
577 | ScheduleInterval
578 | }
579 |
580 | export {
581 | Importance,
582 | Visibility,
583 | sendNotification,
584 | requestPermission,
585 | isPermissionGranted,
586 | registerActionTypes,
587 | pending,
588 | cancel,
589 | cancelAll,
590 | active,
591 | removeActive,
592 | removeAllActive,
593 | createChannel,
594 | removeChannel,
595 | channels,
596 | onNotificationReceived,
597 | onAction,
598 | Schedule,
599 | ScheduleEvery
600 | }
601 |
--------------------------------------------------------------------------------
/guest-js/init.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | import { invoke } from '@tauri-apps/api/core'
6 | import type { PermissionState } from '@tauri-apps/api/core'
7 | import type { Options } from './index'
8 | ;(function () {
9 | let permissionSettable = false
10 | let permissionValue = 'default'
11 |
12 | async function isPermissionGranted(): Promise {
13 | // @ts-expect-error __TEMPLATE_windows__ will be replaced in rust before it's injected.
14 | if (window.Notification.permission !== 'default' || __TEMPLATE_windows__) {
15 | return await Promise.resolve(window.Notification.permission === 'granted')
16 | }
17 | return await invoke('plugin:notification|is_permission_granted')
18 | }
19 |
20 | function setNotificationPermission(value: NotificationPermission): void {
21 | permissionSettable = true
22 | // @ts-expect-error we can actually set this value on the webview
23 | window.Notification.permission = value
24 | permissionSettable = false
25 | }
26 |
27 | async function requestPermission(): Promise {
28 | return await invoke(
29 | 'plugin:notification|request_permission'
30 | ).then((permission) => {
31 | setNotificationPermission(
32 | permission === 'prompt' || permission === 'prompt-with-rationale'
33 | ? 'default'
34 | : permission
35 | )
36 | return permission
37 | })
38 | }
39 |
40 | async function sendNotification(options: string | Options): Promise {
41 | if (typeof options === 'object') {
42 | Object.freeze(options)
43 | }
44 |
45 | await invoke('plugin:notification|notify', {
46 | options:
47 | typeof options === 'string'
48 | ? {
49 | title: options
50 | }
51 | : options
52 | })
53 | }
54 |
55 | // @ts-expect-error unfortunately we can't implement the whole type, so we overwrite it with our own version
56 | window.Notification = function (title, options) {
57 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
58 | const opts = options || {}
59 | void sendNotification(
60 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
61 | Object.assign(opts, {
62 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
63 | title
64 | })
65 | )
66 | }
67 |
68 | // @ts-expect-error tauri does not have sync IPC :(
69 | window.Notification.requestPermission = requestPermission
70 |
71 | Object.defineProperty(window.Notification, 'permission', {
72 | enumerable: true,
73 | get: () => permissionValue,
74 | set: (v) => {
75 | if (!permissionSettable) {
76 | throw new Error('Readonly property')
77 | }
78 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
79 | permissionValue = v
80 | }
81 | })
82 |
83 | void isPermissionGranted().then(function (response) {
84 | if (response === null) {
85 | setNotificationPermission('default')
86 | } else {
87 | setNotificationPermission(response ? 'granted' : 'denied')
88 | }
89 | })
90 | })()
91 |
--------------------------------------------------------------------------------
/ios/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/config/registries.json
8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9 | .netrc
10 | Package.resolved
11 |
--------------------------------------------------------------------------------
/ios/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.5
2 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
3 | // SPDX-License-Identifier: Apache-2.0
4 | // SPDX-License-Identifier: MIT
5 |
6 | import PackageDescription
7 |
8 | let package = Package(
9 | name: "tauri-plugin-notification",
10 | platforms: [
11 | .macOS(.v10_13),
12 | .iOS(.v13),
13 | ],
14 | products: [
15 | // Products define the executables and libraries a package produces, and make them visible to other packages.
16 | .library(
17 | name: "tauri-plugin-notification",
18 | type: .static,
19 | targets: ["tauri-plugin-notification"])
20 | ],
21 | dependencies: [
22 | .package(name: "Tauri", path: "../.tauri/tauri-api")
23 | ],
24 | targets: [
25 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
26 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
27 | .target(
28 | name: "tauri-plugin-notification",
29 | dependencies: [
30 | .byName(name: "Tauri")
31 | ],
32 | path: "Sources")
33 | ]
34 | )
35 |
--------------------------------------------------------------------------------
/ios/README.md:
--------------------------------------------------------------------------------
1 | # Tauri Plugin {{ plugin_name_original }}
2 |
3 | A description of this package.
4 |
--------------------------------------------------------------------------------
/ios/Sources/Notification.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | import Tauri
6 | import UserNotifications
7 |
8 | enum NotificationError: LocalizedError {
9 | case triggerRepeatIntervalTooShort
10 | case attachmentFileNotFound(path: String)
11 | case attachmentUnableToCreate(String)
12 | case pastScheduledTime
13 | case invalidDate(String)
14 |
15 | var errorDescription: String? {
16 | switch self {
17 | case .triggerRepeatIntervalTooShort:
18 | return "Schedule interval too short, must be a least 1 minute"
19 | case .attachmentFileNotFound(let path):
20 | return "Unable to find file \(path) for attachment"
21 | case .attachmentUnableToCreate(let error):
22 | return "Failed to create attachment: \(error)"
23 | case .pastScheduledTime:
24 | return "Scheduled time must be *after* current time"
25 | case .invalidDate(let date):
26 | return "Could not parse date \(date)"
27 | }
28 | }
29 | }
30 |
31 | func makeNotificationContent(_ notification: Notification) throws -> UNNotificationContent {
32 | let content = UNMutableNotificationContent()
33 | content.title = NSString.localizedUserNotificationString(
34 | forKey: notification.title, arguments: nil)
35 | if let body = notification.body {
36 | content.body = NSString.localizedUserNotificationString(
37 | forKey: body,
38 | arguments: nil)
39 | }
40 |
41 | content.userInfo = [
42 | "__EXTRA__": notification.extra as Any,
43 | "__SCHEDULE__": notification.schedule as Any,
44 | ]
45 |
46 | if let actionTypeId = notification.actionTypeId {
47 | content.categoryIdentifier = actionTypeId
48 | }
49 |
50 | if let threadIdentifier = notification.group {
51 | content.threadIdentifier = threadIdentifier
52 | }
53 |
54 | if let summaryArgument = notification.summary {
55 | content.summaryArgument = summaryArgument
56 | }
57 |
58 | if let sound = notification.sound {
59 | content.sound = UNNotificationSound(named: UNNotificationSoundName(sound))
60 | }
61 |
62 | if let attachments = notification.attachments {
63 | content.attachments = try makeAttachments(attachments)
64 | }
65 |
66 | return content
67 | }
68 |
69 | func makeAttachments(_ attachments: [NotificationAttachment]) throws -> [UNNotificationAttachment] {
70 | var createdAttachments = [UNNotificationAttachment]()
71 |
72 | for attachment in attachments {
73 |
74 | guard let urlObject = makeAttachmentUrl(attachment.url) else {
75 | throw NotificationError.attachmentFileNotFound(path: attachment.url)
76 | }
77 |
78 | let options = attachment.options != nil ? makeAttachmentOptions(attachment.options!) : nil
79 |
80 | do {
81 | let newAttachment = try UNNotificationAttachment(
82 | identifier: attachment.id, url: urlObject, options: options)
83 | createdAttachments.append(newAttachment)
84 | } catch {
85 | throw NotificationError.attachmentUnableToCreate(error.localizedDescription)
86 | }
87 | }
88 |
89 | return createdAttachments
90 | }
91 |
92 | func makeAttachmentUrl(_ path: String) -> URL? {
93 | return URL(string: path)
94 | }
95 |
96 | func makeAttachmentOptions(_ options: NotificationAttachmentOptions) -> [AnyHashable: Any] {
97 | var opts: [AnyHashable: Any] = [:]
98 |
99 | if let value = options.iosUNNotificationAttachmentOptionsTypeHintKey {
100 | opts[UNNotificationAttachmentOptionsTypeHintKey] = value
101 | }
102 | if let value = options.iosUNNotificationAttachmentOptionsThumbnailHiddenKey {
103 | opts[UNNotificationAttachmentOptionsThumbnailHiddenKey] = value
104 | }
105 | if let value = options.iosUNNotificationAttachmentOptionsThumbnailClippingRectKey {
106 | opts[UNNotificationAttachmentOptionsThumbnailClippingRectKey] = value
107 | }
108 | if let value = options
109 | .iosUNNotificationAttachmentOptionsThumbnailTimeKey
110 |
111 | {
112 | opts[UNNotificationAttachmentOptionsThumbnailTimeKey] = value
113 | }
114 | return opts
115 | }
116 |
117 | func handleScheduledNotification(_ schedule: NotificationSchedule) throws
118 | -> UNNotificationTrigger?
119 | {
120 | switch schedule {
121 | case .at(let date, let repeating):
122 | let dateFormatter = DateFormatter()
123 | dateFormatter.locale = Locale(identifier: "en_US_POSIX")
124 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
125 |
126 | if let at = dateFormatter.date(from: date) {
127 | let dateInfo = Calendar.current.dateComponents(in: TimeZone.current, from: at)
128 |
129 | if dateInfo.date! < Date() {
130 | throw NotificationError.pastScheduledTime
131 | }
132 |
133 | let dateInterval = DateInterval(start: Date(), end: dateInfo.date!)
134 |
135 | // Notifications that repeat have to be at least a minute between each other
136 | if repeating && dateInterval.duration < 60 {
137 | throw NotificationError.triggerRepeatIntervalTooShort
138 | }
139 |
140 | return UNTimeIntervalNotificationTrigger(
141 | timeInterval: dateInterval.duration, repeats: repeating)
142 |
143 | } else {
144 | throw NotificationError.invalidDate(date)
145 | }
146 | case .interval(let interval):
147 | let dateComponents = getDateComponents(interval)
148 | return UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
149 | case .every(let interval, let count):
150 | if let repeatDateInterval = getRepeatDateInterval(interval, count) {
151 | // Notifications that repeat have to be at least a minute between each other
152 | if repeatDateInterval.duration < 60 {
153 | throw NotificationError.triggerRepeatIntervalTooShort
154 | }
155 |
156 | return UNTimeIntervalNotificationTrigger(
157 | timeInterval: repeatDateInterval.duration, repeats: true)
158 | }
159 | }
160 |
161 | return nil
162 | }
163 |
164 | /// Given our schedule format, return a DateComponents object
165 | /// that only contains the components passed in.
166 |
167 | func getDateComponents(_ at: ScheduleInterval) -> DateComponents {
168 | // var dateInfo = Calendar.current.dateComponents(in: TimeZone.current, from: Date())
169 | // dateInfo.calendar = Calendar.current
170 | var dateInfo = DateComponents()
171 |
172 | if let year = at.year {
173 | dateInfo.year = year
174 | }
175 | if let month = at.month {
176 | dateInfo.month = month
177 | }
178 | if let day = at.day {
179 | dateInfo.day = day
180 | }
181 | if let hour = at.hour {
182 | dateInfo.hour = hour
183 | }
184 | if let minute = at.minute {
185 | dateInfo.minute = minute
186 | }
187 | if let second = at.second {
188 | dateInfo.second = second
189 | }
190 | if let weekday = at.weekday {
191 | dateInfo.weekday = weekday
192 | }
193 | return dateInfo
194 | }
195 |
196 | /// Compute the difference between the string representation of a date
197 | /// interval and today. For example, if every is "month", then we
198 | /// return the interval between today and a month from today.
199 |
200 | func getRepeatDateInterval(_ every: ScheduleEveryKind, _ count: Int) -> DateInterval? {
201 | let cal = Calendar.current
202 | let now = Date()
203 | switch every {
204 | case .year:
205 | let newDate = cal.date(byAdding: .year, value: count, to: now)!
206 | return DateInterval(start: now, end: newDate)
207 | case .month:
208 | let newDate = cal.date(byAdding: .month, value: count, to: now)!
209 | return DateInterval(start: now, end: newDate)
210 | case .twoWeeks:
211 | let newDate = cal.date(byAdding: .weekOfYear, value: 2 * count, to: now)!
212 | return DateInterval(start: now, end: newDate)
213 | case .week:
214 | let newDate = cal.date(byAdding: .weekOfYear, value: count, to: now)!
215 | return DateInterval(start: now, end: newDate)
216 | case .day:
217 | let newDate = cal.date(byAdding: .day, value: count, to: now)!
218 | return DateInterval(start: now, end: newDate)
219 | case .hour:
220 | let newDate = cal.date(byAdding: .hour, value: count, to: now)!
221 | return DateInterval(start: now, end: newDate)
222 | case .minute:
223 | let newDate = cal.date(byAdding: .minute, value: count, to: now)!
224 | return DateInterval(start: now, end: newDate)
225 | case .second:
226 | let newDate = cal.date(byAdding: .second, value: count, to: now)!
227 | return DateInterval(start: now, end: newDate)
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/ios/Sources/NotificationCategory.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | import Tauri
6 | import UserNotifications
7 |
8 | internal func makeCategories(_ actionTypes: [ActionType]) {
9 | var createdCategories = [UNNotificationCategory]()
10 |
11 | let generalCategory = UNNotificationCategory(
12 | identifier: "GENERAL",
13 | actions: [],
14 | intentIdentifiers: [],
15 | options: .customDismissAction)
16 |
17 | createdCategories.append(generalCategory)
18 | for type in actionTypes {
19 | let newActions = makeActions(type.actions)
20 |
21 | // Create the custom actions for the TIMER_EXPIRED category.
22 | var newCategory: UNNotificationCategory?
23 |
24 | newCategory = UNNotificationCategory(
25 | identifier: type.id,
26 | actions: newActions,
27 | intentIdentifiers: [],
28 | hiddenPreviewsBodyPlaceholder: type.hiddenBodyPlaceholder ?? "",
29 | options: makeCategoryOptions(type))
30 |
31 | createdCategories.append(newCategory!)
32 | }
33 |
34 | let center = UNUserNotificationCenter.current()
35 | center.setNotificationCategories(Set(createdCategories))
36 | }
37 |
38 | func makeActions(_ actions: [Action]) -> [UNNotificationAction] {
39 | var createdActions = [UNNotificationAction]()
40 |
41 | for action in actions {
42 | var newAction: UNNotificationAction
43 | if action.input ?? false {
44 | if action.inputButtonTitle != nil {
45 | newAction = UNTextInputNotificationAction(
46 | identifier: action.id,
47 | title: action.title,
48 | options: makeActionOptions(action),
49 | textInputButtonTitle: action.inputButtonTitle ?? "",
50 | textInputPlaceholder: action.inputPlaceholder ?? "")
51 | } else {
52 | newAction = UNTextInputNotificationAction(
53 | identifier: action.id, title: action.title, options: makeActionOptions(action))
54 | }
55 | } else {
56 | // Create the custom actions for the TIMER_EXPIRED category.
57 | newAction = UNNotificationAction(
58 | identifier: action.id,
59 | title: action.title,
60 | options: makeActionOptions(action))
61 | }
62 | createdActions.append(newAction)
63 | }
64 |
65 | return createdActions
66 | }
67 |
68 | func makeActionOptions(_ action: Action) -> UNNotificationActionOptions {
69 | if action.foreground ?? false {
70 | return .foreground
71 | }
72 | if action.destructive ?? false {
73 | return .destructive
74 | }
75 | if action.requiresAuthentication ?? false {
76 | return .authenticationRequired
77 | }
78 | return UNNotificationActionOptions(rawValue: 0)
79 | }
80 |
81 | func makeCategoryOptions(_ type: ActionType) -> UNNotificationCategoryOptions {
82 | if type.customDismissAction ?? false {
83 | return .customDismissAction
84 | }
85 | if type.allowInCarPlay ?? false {
86 | return .allowInCarPlay
87 | }
88 |
89 | if type.hiddenPreviewsShowTitle ?? false {
90 | return .hiddenPreviewsShowTitle
91 | }
92 | if type.hiddenPreviewsShowSubtitle ?? false {
93 | return .hiddenPreviewsShowSubtitle
94 | }
95 |
96 | return UNNotificationCategoryOptions(rawValue: 0)
97 | }
98 |
--------------------------------------------------------------------------------
/ios/Sources/NotificationHandler.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | import Tauri
6 | import UserNotifications
7 |
8 | public class NotificationHandler: NSObject, NotificationHandlerProtocol {
9 |
10 | public weak var plugin: Plugin?
11 |
12 | private var notificationsMap = [String: Notification]()
13 |
14 | internal func saveNotification(_ key: String, _ notification: Notification) {
15 | notificationsMap.updateValue(notification, forKey: key)
16 | }
17 |
18 | public func requestPermissions(with completion: ((Bool, Error?) -> Void)? = nil) {
19 | let center = UNUserNotificationCenter.current()
20 | center.requestAuthorization(options: [.badge, .alert, .sound]) { (granted, error) in
21 | completion?(granted, error)
22 | }
23 | }
24 |
25 | public func checkPermissions(with completion: ((UNAuthorizationStatus) -> Void)? = nil) {
26 | let center = UNUserNotificationCenter.current()
27 | center.getNotificationSettings { settings in
28 | completion?(settings.authorizationStatus)
29 | }
30 | }
31 |
32 | public func willPresent(notification: UNNotification) -> UNNotificationPresentationOptions {
33 | let notificationData = toActiveNotification(notification.request)
34 | try? self.plugin?.trigger("notification", data: notificationData)
35 |
36 | if let options = notificationsMap[notification.request.identifier] {
37 | if options.silent ?? false {
38 | return UNNotificationPresentationOptions.init(rawValue: 0)
39 | }
40 | }
41 |
42 | return [
43 | .badge,
44 | .sound,
45 | .alert,
46 | ]
47 | }
48 |
49 | public func didReceive(response: UNNotificationResponse) {
50 | let originalNotificationRequest = response.notification.request
51 | let actionId = response.actionIdentifier
52 |
53 | var actionIdValue: String
54 | // We turn the two default actions (open/dismiss) into generic strings
55 | if actionId == UNNotificationDefaultActionIdentifier {
56 | actionIdValue = "tap"
57 | } else if actionId == UNNotificationDismissActionIdentifier {
58 | actionIdValue = "dismiss"
59 | } else {
60 | actionIdValue = actionId
61 | }
62 |
63 | var inputValue: String? = nil
64 | // If the type of action was for an input type, get the value
65 | if let inputType = response as? UNTextInputNotificationResponse {
66 | inputValue = inputType.userText
67 | }
68 |
69 | try? self.plugin?.trigger(
70 | "actionPerformed",
71 | data: ReceivedNotification(
72 | actionId: actionIdValue,
73 | inputValue: inputValue,
74 | notification: toActiveNotification(originalNotificationRequest)
75 | ))
76 | }
77 |
78 | func toActiveNotification(_ request: UNNotificationRequest) -> ActiveNotification {
79 | let notificationRequest = notificationsMap[request.identifier]!
80 | return ActiveNotification(
81 | id: Int(request.identifier) ?? -1,
82 | title: request.content.title,
83 | body: request.content.body,
84 | sound: notificationRequest.sound ?? "",
85 | actionTypeId: request.content.categoryIdentifier,
86 | attachments: notificationRequest.attachments
87 | )
88 | }
89 |
90 | func toPendingNotification(_ request: UNNotificationRequest) -> PendingNotification {
91 | return PendingNotification(
92 | id: Int(request.identifier) ?? -1,
93 | title: request.content.title,
94 | body: request.content.body
95 | )
96 | }
97 | }
98 |
99 | struct PendingNotification: Encodable {
100 | let id: Int
101 | let title: String
102 | let body: String
103 | }
104 |
105 | struct ActiveNotification: Encodable {
106 | let id: Int
107 | let title: String
108 | let body: String
109 | let sound: String
110 | let actionTypeId: String
111 | let attachments: [NotificationAttachment]?
112 | }
113 |
114 | struct ReceivedNotification: Encodable {
115 | let actionId: String
116 | let inputValue: String?
117 | let notification: ActiveNotification
118 | }
119 |
--------------------------------------------------------------------------------
/ios/Sources/NotificationManager.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | import Foundation
6 | import UserNotifications
7 |
8 | @objc public protocol NotificationHandlerProtocol {
9 | func willPresent(notification: UNNotification) -> UNNotificationPresentationOptions
10 | func didReceive(response: UNNotificationResponse)
11 | }
12 |
13 | @objc public class NotificationManager: NSObject, UNUserNotificationCenterDelegate {
14 | public weak var notificationHandler: NotificationHandlerProtocol?
15 |
16 | override init() {
17 | super.init()
18 | let center = UNUserNotificationCenter.current()
19 | center.delegate = self
20 | }
21 |
22 | public func userNotificationCenter(
23 | _ center: UNUserNotificationCenter,
24 | willPresent notification: UNNotification,
25 | withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
26 | ) {
27 | var presentationOptions: UNNotificationPresentationOptions? = nil
28 |
29 | if notification.request.trigger?.isKind(of: UNPushNotificationTrigger.self) != true {
30 | presentationOptions = notificationHandler?.willPresent(notification: notification)
31 | }
32 |
33 | completionHandler(presentationOptions ?? [])
34 | }
35 |
36 | public func userNotificationCenter(
37 | _ center: UNUserNotificationCenter,
38 | didReceive response: UNNotificationResponse,
39 | withCompletionHandler completionHandler: @escaping () -> Void
40 | ) {
41 | if response.notification.request.trigger?.isKind(of: UNPushNotificationTrigger.self) != true {
42 | notificationHandler?.didReceive(response: response)
43 | }
44 |
45 | completionHandler()
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/ios/Sources/NotificationPlugin.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | import SwiftRs
6 | import Tauri
7 | import UIKit
8 | import UserNotifications
9 | import WebKit
10 |
11 | enum ShowNotificationError: LocalizedError {
12 | case make(Error)
13 | case create(Error)
14 |
15 | var errorDescription: String? {
16 | switch self {
17 | case .make(let error):
18 | return "Unable to make notification: \(error)"
19 | case .create(let error):
20 | return "Unable to create notification: \(error)"
21 | }
22 | }
23 | }
24 |
25 | enum ScheduleEveryKind: String, Decodable {
26 | case year
27 | case month
28 | case twoWeeks
29 | case week
30 | case day
31 | case hour
32 | case minute
33 | case second
34 | }
35 |
36 | struct ScheduleInterval: Decodable {
37 | var year: Int?
38 | var month: Int?
39 | var day: Int?
40 | var weekday: Int?
41 | var hour: Int?
42 | var minute: Int?
43 | var second: Int?
44 | }
45 |
46 | enum NotificationSchedule: Decodable {
47 | case at(date: String, repeating: Bool)
48 | case interval(interval: ScheduleInterval)
49 | case every(interval: ScheduleEveryKind, count: Int)
50 | }
51 |
52 | struct NotificationAttachmentOptions: Codable {
53 | let iosUNNotificationAttachmentOptionsTypeHintKey: String?
54 | let iosUNNotificationAttachmentOptionsThumbnailHiddenKey: String?
55 | let iosUNNotificationAttachmentOptionsThumbnailClippingRectKey: String?
56 | let iosUNNotificationAttachmentOptionsThumbnailTimeKey: String?
57 | }
58 |
59 | struct NotificationAttachment: Codable {
60 | let id: String
61 | let url: String
62 | let options: NotificationAttachmentOptions?
63 | }
64 |
65 | struct Notification: Decodable {
66 | let id: Int
67 | var title: String
68 | var body: String?
69 | var extra: [String: String]?
70 | var schedule: NotificationSchedule?
71 | var attachments: [NotificationAttachment]?
72 | var sound: String?
73 | var group: String?
74 | var actionTypeId: String?
75 | var summary: String?
76 | var silent: Bool?
77 | }
78 |
79 | struct RemoveActiveNotification: Decodable {
80 | let id: Int
81 | }
82 |
83 | struct RemoveActiveArgs: Decodable {
84 | let notifications: [RemoveActiveNotification]
85 | }
86 |
87 | func showNotification(invoke: Invoke, notification: Notification)
88 | throws -> UNNotificationRequest
89 | {
90 | var content: UNNotificationContent
91 | do {
92 | content = try makeNotificationContent(notification)
93 | } catch {
94 | throw ShowNotificationError.make(error)
95 | }
96 |
97 | var trigger: UNNotificationTrigger?
98 |
99 | do {
100 | if let schedule = notification.schedule {
101 | try trigger = handleScheduledNotification(schedule)
102 | }
103 | } catch {
104 | throw ShowNotificationError.create(error)
105 | }
106 |
107 | // Schedule the request.
108 | let request = UNNotificationRequest(
109 | identifier: "\(notification.id)", content: content, trigger: trigger
110 | )
111 |
112 | let center = UNUserNotificationCenter.current()
113 | center.add(request) { (error: Error?) in
114 | if let theError = error {
115 | invoke.reject(theError.localizedDescription)
116 | }
117 | }
118 |
119 | return request
120 | }
121 |
122 | struct CancelArgs: Decodable {
123 | let notifications: [Int]
124 | }
125 |
126 | struct Action: Decodable {
127 | let id: String
128 | let title: String
129 | var requiresAuthentication: Bool?
130 | var foreground: Bool?
131 | var destructive: Bool?
132 | var input: Bool?
133 | var inputButtonTitle: String?
134 | var inputPlaceholder: String?
135 | }
136 |
137 | struct ActionType: Decodable {
138 | let id: String
139 | let actions: [Action]
140 | var hiddenPreviewsBodyPlaceholder: String?
141 | var customDismissAction: Bool?
142 | var allowInCarPlay: Bool?
143 | var hiddenPreviewsShowTitle: Bool?
144 | var hiddenPreviewsShowSubtitle: Bool?
145 | var hiddenBodyPlaceholder: String?
146 | }
147 |
148 | struct RegisterActionTypesArgs: Decodable {
149 | let types: [ActionType]
150 | }
151 |
152 | struct BatchArgs: Decodable {
153 | let notifications: [Notification]
154 | }
155 |
156 | class NotificationPlugin: Plugin {
157 | let notificationHandler = NotificationHandler()
158 | let notificationManager = NotificationManager()
159 |
160 | override init() {
161 | super.init()
162 | notificationManager.notificationHandler = notificationHandler
163 | notificationHandler.plugin = self
164 | }
165 |
166 | @objc public func show(_ invoke: Invoke) throws {
167 | let notification = try invoke.parseArgs(Notification.self)
168 |
169 | let request = try showNotification(invoke: invoke, notification: notification)
170 | notificationHandler.saveNotification(request.identifier, notification)
171 | invoke.resolve(Int(request.identifier) ?? -1)
172 | }
173 |
174 | @objc public func batch(_ invoke: Invoke) throws {
175 | let args = try invoke.parseArgs(BatchArgs.self)
176 | var ids = [Int]()
177 |
178 | for notification in args.notifications {
179 | let request = try showNotification(invoke: invoke, notification: notification)
180 | notificationHandler.saveNotification(request.identifier, notification)
181 | ids.append(Int(request.identifier) ?? -1)
182 | }
183 |
184 | invoke.resolve(ids)
185 | }
186 |
187 | @objc public override func requestPermissions(_ invoke: Invoke) {
188 | notificationHandler.requestPermissions { granted, error in
189 | guard error == nil else {
190 | invoke.reject(error!.localizedDescription)
191 | return
192 | }
193 | invoke.resolve(["permissionState": granted ? "granted" : "denied"])
194 | }
195 | }
196 |
197 | @objc public override func checkPermissions(_ invoke: Invoke) {
198 | notificationHandler.checkPermissions { status in
199 | let permission: String
200 |
201 | switch status {
202 | case .authorized, .ephemeral, .provisional:
203 | permission = "granted"
204 | case .denied:
205 | permission = "denied"
206 | case .notDetermined:
207 | permission = "prompt"
208 | @unknown default:
209 | permission = "prompt"
210 | }
211 |
212 | invoke.resolve(["permissionState": permission])
213 | }
214 | }
215 |
216 | @objc func cancel(_ invoke: Invoke) throws {
217 | let args = try invoke.parseArgs(CancelArgs.self)
218 |
219 | UNUserNotificationCenter.current().removePendingNotificationRequests(
220 | withIdentifiers: args.notifications.map { String($0) }
221 | )
222 | invoke.resolve()
223 | }
224 |
225 | @objc func getPending(_ invoke: Invoke) {
226 | UNUserNotificationCenter.current().getPendingNotificationRequests(completionHandler: {
227 | (notifications) in
228 | let ret = notifications.compactMap({ [weak self] (notification) -> PendingNotification? in
229 | return self?.notificationHandler.toPendingNotification(notification)
230 | })
231 |
232 | invoke.resolve(ret)
233 | })
234 | }
235 |
236 | @objc func registerActionTypes(_ invoke: Invoke) throws {
237 | let args = try invoke.parseArgs(RegisterActionTypesArgs.self)
238 | makeCategories(args.types)
239 | invoke.resolve()
240 | }
241 |
242 | @objc func removeActive(_ invoke: Invoke) {
243 | do {
244 | let args = try invoke.parseArgs(RemoveActiveArgs.self)
245 | UNUserNotificationCenter.current().removeDeliveredNotifications(
246 | withIdentifiers: args.notifications.map { String($0.id) })
247 | invoke.resolve()
248 | } catch {
249 | UNUserNotificationCenter.current().removeAllDeliveredNotifications()
250 | DispatchQueue.main.async(execute: {
251 | UIApplication.shared.applicationIconBadgeNumber = 0
252 | })
253 | invoke.resolve()
254 | }
255 | }
256 |
257 | @objc func getActive(_ invoke: Invoke) {
258 | UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: {
259 | (notifications) in
260 | let ret = notifications.map({ (notification) -> ActiveNotification in
261 | return self.notificationHandler.toActiveNotification(
262 | notification.request)
263 | })
264 | invoke.resolve(ret)
265 | })
266 | }
267 |
268 | @objc func createChannel(_ invoke: Invoke) {
269 | invoke.reject("not implemented")
270 | }
271 |
272 | @objc func deleteChannel(_ invoke: Invoke) {
273 | invoke.reject("not implemented")
274 | }
275 |
276 | @objc func listChannels(_ invoke: Invoke) {
277 | invoke.reject("not implemented")
278 | }
279 |
280 | }
281 |
282 | @_cdecl("init_plugin_notification")
283 | func initPlugin() -> Plugin {
284 | return NotificationPlugin()
285 | }
286 |
--------------------------------------------------------------------------------
/ios/Tests/PluginTests/PluginTests.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | import XCTest
6 | @testable import ExamplePlugin
7 |
8 | final class ExamplePluginTests: XCTestCase {
9 | func testExample() throws {
10 | let plugin = ExamplePlugin()
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/node_modules/@tauri-apps/api:
--------------------------------------------------------------------------------
1 | ../../../../node_modules/.pnpm/@tauri-apps+api@2.5.0/node_modules/@tauri-apps/api
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tauri-apps/plugin-notification",
3 | "version": "2.2.2",
4 | "license": "MIT OR Apache-2.0",
5 | "authors": [
6 | "Tauri Programme within The Commons Conservancy"
7 | ],
8 | "repository": "https://github.com/tauri-apps/plugins-workspace",
9 | "type": "module",
10 | "types": "./dist-js/index.d.ts",
11 | "main": "./dist-js/index.cjs",
12 | "module": "./dist-js/index.js",
13 | "exports": {
14 | "types": "./dist-js/index.d.ts",
15 | "import": "./dist-js/index.js",
16 | "require": "./dist-js/index.cjs"
17 | },
18 | "scripts": {
19 | "build": "rollup -c"
20 | },
21 | "files": [
22 | "dist-js",
23 | "README.md",
24 | "LICENSE"
25 | ],
26 | "dependencies": {
27 | "@tauri-apps/api": "^2.0.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/permissions/autogenerated/commands/batch.toml:
--------------------------------------------------------------------------------
1 | # Automatically generated - DO NOT EDIT!
2 |
3 | "$schema" = "../../schemas/schema.json"
4 |
5 | [[permission]]
6 | identifier = "allow-batch"
7 | description = "Enables the batch command without any pre-configured scope."
8 | commands.allow = ["batch"]
9 |
10 | [[permission]]
11 | identifier = "deny-batch"
12 | description = "Denies the batch command without any pre-configured scope."
13 | commands.deny = ["batch"]
14 |
--------------------------------------------------------------------------------
/permissions/autogenerated/commands/cancel.toml:
--------------------------------------------------------------------------------
1 | # Automatically generated - DO NOT EDIT!
2 |
3 | "$schema" = "../../schemas/schema.json"
4 |
5 | [[permission]]
6 | identifier = "allow-cancel"
7 | description = "Enables the cancel command without any pre-configured scope."
8 | commands.allow = ["cancel"]
9 |
10 | [[permission]]
11 | identifier = "deny-cancel"
12 | description = "Denies the cancel command without any pre-configured scope."
13 | commands.deny = ["cancel"]
14 |
--------------------------------------------------------------------------------
/permissions/autogenerated/commands/check_permissions.toml:
--------------------------------------------------------------------------------
1 | # Automatically generated - DO NOT EDIT!
2 |
3 | "$schema" = "../../schemas/schema.json"
4 |
5 | [[permission]]
6 | identifier = "allow-check-permissions"
7 | description = "Enables the check_permissions command without any pre-configured scope."
8 | commands.allow = ["check_permissions"]
9 |
10 | [[permission]]
11 | identifier = "deny-check-permissions"
12 | description = "Denies the check_permissions command without any pre-configured scope."
13 | commands.deny = ["check_permissions"]
14 |
--------------------------------------------------------------------------------
/permissions/autogenerated/commands/create_channel.toml:
--------------------------------------------------------------------------------
1 | # Automatically generated - DO NOT EDIT!
2 |
3 | "$schema" = "../../schemas/schema.json"
4 |
5 | [[permission]]
6 | identifier = "allow-create-channel"
7 | description = "Enables the create_channel command without any pre-configured scope."
8 | commands.allow = ["create_channel"]
9 |
10 | [[permission]]
11 | identifier = "deny-create-channel"
12 | description = "Denies the create_channel command without any pre-configured scope."
13 | commands.deny = ["create_channel"]
14 |
--------------------------------------------------------------------------------
/permissions/autogenerated/commands/delete_channel.toml:
--------------------------------------------------------------------------------
1 | # Automatically generated - DO NOT EDIT!
2 |
3 | "$schema" = "../../schemas/schema.json"
4 |
5 | [[permission]]
6 | identifier = "allow-delete-channel"
7 | description = "Enables the delete_channel command without any pre-configured scope."
8 | commands.allow = ["delete_channel"]
9 |
10 | [[permission]]
11 | identifier = "deny-delete-channel"
12 | description = "Denies the delete_channel command without any pre-configured scope."
13 | commands.deny = ["delete_channel"]
14 |
--------------------------------------------------------------------------------
/permissions/autogenerated/commands/get_active.toml:
--------------------------------------------------------------------------------
1 | # Automatically generated - DO NOT EDIT!
2 |
3 | "$schema" = "../../schemas/schema.json"
4 |
5 | [[permission]]
6 | identifier = "allow-get-active"
7 | description = "Enables the get_active command without any pre-configured scope."
8 | commands.allow = ["get_active"]
9 |
10 | [[permission]]
11 | identifier = "deny-get-active"
12 | description = "Denies the get_active command without any pre-configured scope."
13 | commands.deny = ["get_active"]
14 |
--------------------------------------------------------------------------------
/permissions/autogenerated/commands/get_pending.toml:
--------------------------------------------------------------------------------
1 | # Automatically generated - DO NOT EDIT!
2 |
3 | "$schema" = "../../schemas/schema.json"
4 |
5 | [[permission]]
6 | identifier = "allow-get-pending"
7 | description = "Enables the get_pending command without any pre-configured scope."
8 | commands.allow = ["get_pending"]
9 |
10 | [[permission]]
11 | identifier = "deny-get-pending"
12 | description = "Denies the get_pending command without any pre-configured scope."
13 | commands.deny = ["get_pending"]
14 |
--------------------------------------------------------------------------------
/permissions/autogenerated/commands/is_permission_granted.toml:
--------------------------------------------------------------------------------
1 | # Automatically generated - DO NOT EDIT!
2 |
3 | "$schema" = "../../schemas/schema.json"
4 |
5 | [[permission]]
6 | identifier = "allow-is-permission-granted"
7 | description = "Enables the is_permission_granted command without any pre-configured scope."
8 | commands.allow = ["is_permission_granted"]
9 |
10 | [[permission]]
11 | identifier = "deny-is-permission-granted"
12 | description = "Denies the is_permission_granted command without any pre-configured scope."
13 | commands.deny = ["is_permission_granted"]
14 |
--------------------------------------------------------------------------------
/permissions/autogenerated/commands/list_channels.toml:
--------------------------------------------------------------------------------
1 | # Automatically generated - DO NOT EDIT!
2 |
3 | "$schema" = "../../schemas/schema.json"
4 |
5 | [[permission]]
6 | identifier = "allow-list-channels"
7 | description = "Enables the list_channels command without any pre-configured scope."
8 | commands.allow = ["list_channels"]
9 |
10 | [[permission]]
11 | identifier = "deny-list-channels"
12 | description = "Denies the list_channels command without any pre-configured scope."
13 | commands.deny = ["list_channels"]
14 |
--------------------------------------------------------------------------------
/permissions/autogenerated/commands/notify.toml:
--------------------------------------------------------------------------------
1 | # Automatically generated - DO NOT EDIT!
2 |
3 | "$schema" = "../../schemas/schema.json"
4 |
5 | [[permission]]
6 | identifier = "allow-notify"
7 | description = "Enables the notify command without any pre-configured scope."
8 | commands.allow = ["notify"]
9 |
10 | [[permission]]
11 | identifier = "deny-notify"
12 | description = "Denies the notify command without any pre-configured scope."
13 | commands.deny = ["notify"]
14 |
--------------------------------------------------------------------------------
/permissions/autogenerated/commands/permission_state.toml:
--------------------------------------------------------------------------------
1 | # Automatically generated - DO NOT EDIT!
2 |
3 | "$schema" = "../../schemas/schema.json"
4 |
5 | [[permission]]
6 | identifier = "allow-permission-state"
7 | description = "Enables the permission_state command without any pre-configured scope."
8 | commands.allow = ["permission_state"]
9 |
10 | [[permission]]
11 | identifier = "deny-permission-state"
12 | description = "Denies the permission_state command without any pre-configured scope."
13 | commands.deny = ["permission_state"]
14 |
--------------------------------------------------------------------------------
/permissions/autogenerated/commands/register_action_types.toml:
--------------------------------------------------------------------------------
1 | # Automatically generated - DO NOT EDIT!
2 |
3 | "$schema" = "../../schemas/schema.json"
4 |
5 | [[permission]]
6 | identifier = "allow-register-action-types"
7 | description = "Enables the register_action_types command without any pre-configured scope."
8 | commands.allow = ["register_action_types"]
9 |
10 | [[permission]]
11 | identifier = "deny-register-action-types"
12 | description = "Denies the register_action_types command without any pre-configured scope."
13 | commands.deny = ["register_action_types"]
14 |
--------------------------------------------------------------------------------
/permissions/autogenerated/commands/register_listener.toml:
--------------------------------------------------------------------------------
1 | # Automatically generated - DO NOT EDIT!
2 |
3 | "$schema" = "../../schemas/schema.json"
4 |
5 | [[permission]]
6 | identifier = "allow-register-listener"
7 | description = "Enables the register_listener command without any pre-configured scope."
8 | commands.allow = ["register_listener"]
9 |
10 | [[permission]]
11 | identifier = "deny-register-listener"
12 | description = "Denies the register_listener command without any pre-configured scope."
13 | commands.deny = ["register_listener"]
14 |
--------------------------------------------------------------------------------
/permissions/autogenerated/commands/remove_active.toml:
--------------------------------------------------------------------------------
1 | # Automatically generated - DO NOT EDIT!
2 |
3 | "$schema" = "../../schemas/schema.json"
4 |
5 | [[permission]]
6 | identifier = "allow-remove-active"
7 | description = "Enables the remove_active command without any pre-configured scope."
8 | commands.allow = ["remove_active"]
9 |
10 | [[permission]]
11 | identifier = "deny-remove-active"
12 | description = "Denies the remove_active command without any pre-configured scope."
13 | commands.deny = ["remove_active"]
14 |
--------------------------------------------------------------------------------
/permissions/autogenerated/commands/request_permission.toml:
--------------------------------------------------------------------------------
1 | # Automatically generated - DO NOT EDIT!
2 |
3 | "$schema" = "../../schemas/schema.json"
4 |
5 | [[permission]]
6 | identifier = "allow-request-permission"
7 | description = "Enables the request_permission command without any pre-configured scope."
8 | commands.allow = ["request_permission"]
9 |
10 | [[permission]]
11 | identifier = "deny-request-permission"
12 | description = "Denies the request_permission command without any pre-configured scope."
13 | commands.deny = ["request_permission"]
14 |
--------------------------------------------------------------------------------
/permissions/autogenerated/commands/show.toml:
--------------------------------------------------------------------------------
1 | # Automatically generated - DO NOT EDIT!
2 |
3 | "$schema" = "../../schemas/schema.json"
4 |
5 | [[permission]]
6 | identifier = "allow-show"
7 | description = "Enables the show command without any pre-configured scope."
8 | commands.allow = ["show"]
9 |
10 | [[permission]]
11 | identifier = "deny-show"
12 | description = "Denies the show command without any pre-configured scope."
13 | commands.deny = ["show"]
14 |
--------------------------------------------------------------------------------
/permissions/autogenerated/reference.md:
--------------------------------------------------------------------------------
1 | ## Default Permission
2 |
3 | This permission set configures which
4 | notification features are by default exposed.
5 |
6 | #### Granted Permissions
7 |
8 | It allows all notification related features.
9 |
10 |
11 |
12 | #### This default permission set includes the following:
13 |
14 | - `allow-is-permission-granted`
15 | - `allow-request-permission`
16 | - `allow-notify`
17 | - `allow-register-action-types`
18 | - `allow-register-listener`
19 | - `allow-cancel`
20 | - `allow-get-pending`
21 | - `allow-remove-active`
22 | - `allow-get-active`
23 | - `allow-check-permissions`
24 | - `allow-show`
25 | - `allow-batch`
26 | - `allow-list-channels`
27 | - `allow-delete-channel`
28 | - `allow-create-channel`
29 | - `allow-permission-state`
30 |
31 | ## Permission Table
32 |
33 |
34 |
35 | Identifier
36 | Description
37 |
38 |
39 |
40 |
41 |
42 |
43 | `notification:allow-batch`
44 |
45 |
46 |
47 |
48 | Enables the batch command without any pre-configured scope.
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | `notification:deny-batch`
57 |
58 |
59 |
60 |
61 | Denies the batch command without any pre-configured scope.
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | `notification:allow-cancel`
70 |
71 |
72 |
73 |
74 | Enables the cancel command without any pre-configured scope.
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | `notification:deny-cancel`
83 |
84 |
85 |
86 |
87 | Denies the cancel command without any pre-configured scope.
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | `notification:allow-check-permissions`
96 |
97 |
98 |
99 |
100 | Enables the check_permissions command without any pre-configured scope.
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | `notification:deny-check-permissions`
109 |
110 |
111 |
112 |
113 | Denies the check_permissions command without any pre-configured scope.
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 | `notification:allow-create-channel`
122 |
123 |
124 |
125 |
126 | Enables the create_channel command without any pre-configured scope.
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 | `notification:deny-create-channel`
135 |
136 |
137 |
138 |
139 | Denies the create_channel command without any pre-configured scope.
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 | `notification:allow-delete-channel`
148 |
149 |
150 |
151 |
152 | Enables the delete_channel command without any pre-configured scope.
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 | `notification:deny-delete-channel`
161 |
162 |
163 |
164 |
165 | Denies the delete_channel command without any pre-configured scope.
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 | `notification:allow-get-active`
174 |
175 |
176 |
177 |
178 | Enables the get_active command without any pre-configured scope.
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 | `notification:deny-get-active`
187 |
188 |
189 |
190 |
191 | Denies the get_active command without any pre-configured scope.
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 | `notification:allow-get-pending`
200 |
201 |
202 |
203 |
204 | Enables the get_pending command without any pre-configured scope.
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 | `notification:deny-get-pending`
213 |
214 |
215 |
216 |
217 | Denies the get_pending command without any pre-configured scope.
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 | `notification:allow-is-permission-granted`
226 |
227 |
228 |
229 |
230 | Enables the is_permission_granted command without any pre-configured scope.
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 | `notification:deny-is-permission-granted`
239 |
240 |
241 |
242 |
243 | Denies the is_permission_granted command without any pre-configured scope.
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 | `notification:allow-list-channels`
252 |
253 |
254 |
255 |
256 | Enables the list_channels command without any pre-configured scope.
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 | `notification:deny-list-channels`
265 |
266 |
267 |
268 |
269 | Denies the list_channels command without any pre-configured scope.
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 | `notification:allow-notify`
278 |
279 |
280 |
281 |
282 | Enables the notify command without any pre-configured scope.
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 | `notification:deny-notify`
291 |
292 |
293 |
294 |
295 | Denies the notify command without any pre-configured scope.
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 | `notification:allow-permission-state`
304 |
305 |
306 |
307 |
308 | Enables the permission_state command without any pre-configured scope.
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 | `notification:deny-permission-state`
317 |
318 |
319 |
320 |
321 | Denies the permission_state command without any pre-configured scope.
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 | `notification:allow-register-action-types`
330 |
331 |
332 |
333 |
334 | Enables the register_action_types command without any pre-configured scope.
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 | `notification:deny-register-action-types`
343 |
344 |
345 |
346 |
347 | Denies the register_action_types command without any pre-configured scope.
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 | `notification:allow-register-listener`
356 |
357 |
358 |
359 |
360 | Enables the register_listener command without any pre-configured scope.
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 | `notification:deny-register-listener`
369 |
370 |
371 |
372 |
373 | Denies the register_listener command without any pre-configured scope.
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 | `notification:allow-remove-active`
382 |
383 |
384 |
385 |
386 | Enables the remove_active command without any pre-configured scope.
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 | `notification:deny-remove-active`
395 |
396 |
397 |
398 |
399 | Denies the remove_active command without any pre-configured scope.
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 | `notification:allow-request-permission`
408 |
409 |
410 |
411 |
412 | Enables the request_permission command without any pre-configured scope.
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 | `notification:deny-request-permission`
421 |
422 |
423 |
424 |
425 | Denies the request_permission command without any pre-configured scope.
426 |
427 |
428 |
429 |
430 |
431 |
432 |
433 | `notification:allow-show`
434 |
435 |
436 |
437 |
438 | Enables the show command without any pre-configured scope.
439 |
440 |
441 |
442 |
443 |
444 |
445 |
446 | `notification:deny-show`
447 |
448 |
449 |
450 |
451 | Denies the show command without any pre-configured scope.
452 |
453 |
454 |
455 |
456 |
--------------------------------------------------------------------------------
/permissions/default.toml:
--------------------------------------------------------------------------------
1 | "$schema" = "schemas/schema.json"
2 | [default]
3 | description = """
4 | This permission set configures which
5 | notification features are by default exposed.
6 |
7 | #### Granted Permissions
8 |
9 | It allows all notification related features.
10 |
11 | """
12 |
13 | permissions = [
14 | "allow-is-permission-granted",
15 | "allow-request-permission",
16 | "allow-notify",
17 | "allow-register-action-types",
18 | "allow-register-listener",
19 | "allow-cancel",
20 | "allow-get-pending",
21 | "allow-remove-active",
22 | "allow-get-active",
23 | "allow-check-permissions",
24 | "allow-show",
25 | "allow-batch",
26 | "allow-list-channels",
27 | "allow-delete-channel",
28 | "allow-create-channel",
29 | "allow-permission-state",
30 | ]
31 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | import { createConfig } from '../../shared/rollup.config.js'
6 | import { nodeResolve } from '@rollup/plugin-node-resolve'
7 | import typescript from '@rollup/plugin-typescript'
8 | import terser from '@rollup/plugin-terser'
9 |
10 | export default createConfig({
11 | additionalConfigs: {
12 | input: 'guest-js/init.ts',
13 | output: {
14 | file: 'src/init-iife.js',
15 | format: 'iife'
16 | },
17 | plugins: [typescript(), terser(), nodeResolve()],
18 | onwarn: (warning) => {
19 | throw Object.assign(new Error(), warning)
20 | }
21 | }
22 | })
23 |
--------------------------------------------------------------------------------
/src/commands.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | use tauri::{command, plugin::PermissionState, AppHandle, Runtime, State};
6 |
7 | use crate::{Notification, NotificationData, Result};
8 |
9 | #[command]
10 | pub(crate) async fn is_permission_granted(
11 | _app: AppHandle,
12 | notification: State<'_, Notification>,
13 | ) -> Result> {
14 | let state = notification.permission_state()?;
15 | match state {
16 | PermissionState::Granted => Ok(Some(true)),
17 | PermissionState::Denied => Ok(Some(false)),
18 | PermissionState::Prompt | PermissionState::PromptWithRationale => Ok(None),
19 | }
20 | }
21 |
22 | #[command]
23 | pub(crate) async fn request_permission(
24 | _app: AppHandle,
25 | notification: State<'_, Notification>,
26 | ) -> Result {
27 | notification.request_permission()
28 | }
29 |
30 | #[command]
31 | pub(crate) async fn notify(
32 | _app: AppHandle,
33 | notification: State<'_, Notification>,
34 | options: NotificationData,
35 | ) -> Result<()> {
36 | let mut builder = notification.builder();
37 | builder.data = options;
38 | builder.show()
39 | }
40 |
--------------------------------------------------------------------------------
/src/desktop.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | use serde::de::DeserializeOwned;
6 | use tauri::{
7 | plugin::{PermissionState, PluginApi},
8 | AppHandle, Runtime,
9 | };
10 |
11 | use crate::NotificationBuilder;
12 |
13 | pub fn init(
14 | app: &AppHandle,
15 | _api: PluginApi,
16 | ) -> crate::Result> {
17 | Ok(Notification(app.clone()))
18 | }
19 |
20 | /// Access to the notification APIs.
21 | ///
22 | /// You can get an instance of this type via [`NotificationExt`](crate::NotificationExt)
23 | pub struct Notification(AppHandle);
24 |
25 | impl crate::NotificationBuilder {
26 | pub fn show(self) -> crate::Result<()> {
27 | let mut notification = imp::Notification::new(self.app.config().identifier.clone());
28 |
29 | if let Some(title) = self
30 | .data
31 | .title
32 | .or_else(|| self.app.config().product_name.clone())
33 | {
34 | notification = notification.title(title);
35 | }
36 | if let Some(body) = self.data.body {
37 | notification = notification.body(body);
38 | }
39 | if let Some(icon) = self.data.icon {
40 | notification = notification.icon(icon);
41 | }
42 | #[cfg(feature = "windows7-compat")]
43 | {
44 | notification.notify(&self.app)?;
45 | }
46 | #[cfg(not(feature = "windows7-compat"))]
47 | notification.show()?;
48 |
49 | Ok(())
50 | }
51 | }
52 |
53 | impl Notification {
54 | pub fn builder(&self) -> NotificationBuilder {
55 | NotificationBuilder::new(self.0.clone())
56 | }
57 |
58 | pub fn request_permission(&self) -> crate::Result {
59 | Ok(PermissionState::Granted)
60 | }
61 |
62 | pub fn permission_state(&self) -> crate::Result {
63 | Ok(PermissionState::Granted)
64 | }
65 | }
66 |
67 | mod imp {
68 | //! Types and functions related to desktop notifications.
69 |
70 | #[cfg(windows)]
71 | use std::path::MAIN_SEPARATOR as SEP;
72 |
73 | /// The desktop notification definition.
74 | ///
75 | /// Allows you to construct a Notification data and send it.
76 | ///
77 | /// # Examples
78 | /// ```rust,no_run
79 | /// use tauri_plugin_notification::NotificationExt;
80 | /// // first we build the application to access the Tauri configuration
81 | /// let app = tauri::Builder::default()
82 | /// // on an actual app, remove the string argument
83 | /// .build(tauri::generate_context!("test/tauri.conf.json"))
84 | /// .expect("error while building tauri application");
85 | ///
86 | /// // shows a notification with the given title and body
87 | /// app.notification()
88 | /// .builder()
89 | /// .title("New message")
90 | /// .body("You've got a new message.")
91 | /// .show();
92 | ///
93 | /// // run the app
94 | /// app.run(|_app_handle, _event| {});
95 | /// ```
96 | #[allow(dead_code)]
97 | #[derive(Debug, Default)]
98 | pub struct Notification {
99 | /// The notification body.
100 | body: Option,
101 | /// The notification title.
102 | title: Option,
103 | /// The notification icon.
104 | icon: Option,
105 | /// The notification identifier
106 | identifier: String,
107 | }
108 |
109 | impl Notification {
110 | /// Initializes a instance of a Notification.
111 | pub fn new(identifier: impl Into) -> Self {
112 | Self {
113 | identifier: identifier.into(),
114 | ..Default::default()
115 | }
116 | }
117 |
118 | /// Sets the notification body.
119 | #[must_use]
120 | pub fn body(mut self, body: impl Into) -> Self {
121 | self.body = Some(body.into());
122 | self
123 | }
124 |
125 | /// Sets the notification title.
126 | #[must_use]
127 | pub fn title(mut self, title: impl Into) -> Self {
128 | self.title = Some(title.into());
129 | self
130 | }
131 |
132 | /// Sets the notification icon.
133 | #[must_use]
134 | pub fn icon(mut self, icon: impl Into) -> Self {
135 | self.icon = Some(icon.into());
136 | self
137 | }
138 |
139 | /// Shows the notification.
140 | ///
141 | /// # Examples
142 | ///
143 | /// ```no_run
144 | /// use tauri_plugin_notification::NotificationExt;
145 | ///
146 | /// tauri::Builder::default()
147 | /// .setup(|app| {
148 | /// app.notification()
149 | /// .builder()
150 | /// .title("Tauri")
151 | /// .body("Tauri is awesome!")
152 | /// .show()
153 | /// .unwrap();
154 | /// Ok(())
155 | /// })
156 | /// .run(tauri::generate_context!("test/tauri.conf.json"))
157 | /// .expect("error while running tauri application");
158 | /// ```
159 | ///
160 | /// ## Platform-specific
161 | ///
162 | /// - **Windows**: Not supported on Windows 7. If your app targets it, enable the `windows7-compat` feature and use [`Self::notify`].
163 | #[cfg_attr(
164 | all(not(docsrs), feature = "windows7-compat"),
165 | deprecated = "This function does not work on Windows 7. Use `Self::notify` instead."
166 | )]
167 | pub fn show(self) -> crate::Result<()> {
168 | let mut notification = notify_rust::Notification::new();
169 | if let Some(body) = self.body {
170 | notification.body(&body);
171 | }
172 | if let Some(title) = self.title {
173 | notification.summary(&title);
174 | }
175 | if let Some(icon) = self.icon {
176 | notification.icon(&icon);
177 | } else {
178 | notification.auto_icon();
179 | }
180 | #[cfg(windows)]
181 | {
182 | let exe = tauri::utils::platform::current_exe()?;
183 | let exe_dir = exe.parent().expect("failed to get exe directory");
184 | let curr_dir = exe_dir.display().to_string();
185 | // set the notification's System.AppUserModel.ID only when running the installed app
186 | if !(curr_dir.ends_with(format!("{SEP}target{SEP}debug").as_str())
187 | || curr_dir.ends_with(format!("{SEP}target{SEP}release").as_str()))
188 | {
189 | notification.app_id(&self.identifier);
190 | }
191 | }
192 | #[cfg(target_os = "macos")]
193 | {
194 | let _ = notify_rust::set_application(if tauri::is_dev() {
195 | "com.apple.Terminal"
196 | } else {
197 | &self.identifier
198 | });
199 | }
200 |
201 | tauri::async_runtime::spawn(async move {
202 | let _ = notification.show();
203 | });
204 |
205 | Ok(())
206 | }
207 |
208 | /// Shows the notification. This API is similar to [`Self::show`], but it also works on Windows 7.
209 | ///
210 | /// # Examples
211 | ///
212 | /// ```no_run
213 | /// use tauri_plugin_notification::NotificationExt;
214 | ///
215 | /// tauri::Builder::default()
216 | /// .setup(move |app| {
217 | /// app.notification().builder()
218 | /// .title("Tauri")
219 | /// .body("Tauri is awesome!")
220 | /// .show()
221 | /// .unwrap();
222 | /// Ok(())
223 | /// })
224 | /// .run(tauri::generate_context!("test/tauri.conf.json"))
225 | /// .expect("error while running tauri application");
226 | /// ```
227 | #[cfg(feature = "windows7-compat")]
228 | #[cfg_attr(docsrs, doc(cfg(feature = "windows7-compat")))]
229 | #[allow(unused_variables)]
230 | pub fn notify(self, app: &tauri::AppHandle) -> crate::Result<()> {
231 | #[cfg(windows)]
232 | {
233 | fn is_windows_7() -> bool {
234 | let v = windows_version::OsVersion::current();
235 | // windows 7 is 6.1
236 | v.major == 6 && v.minor == 1
237 | }
238 |
239 | if is_windows_7() {
240 | self.notify_win7(app)
241 | } else {
242 | #[allow(deprecated)]
243 | self.show()
244 | }
245 | }
246 | #[cfg(not(windows))]
247 | {
248 | #[allow(deprecated)]
249 | self.show()
250 | }
251 | }
252 |
253 | #[cfg(all(windows, feature = "windows7-compat"))]
254 | fn notify_win7(self, app: &tauri::AppHandle) -> crate::Result<()> {
255 | let app_ = app.clone();
256 | let _ = app.clone().run_on_main_thread(move || {
257 | let mut notification = win7_notifications::Notification::new();
258 | if let Some(body) = self.body {
259 | notification.body(&body);
260 | }
261 | if let Some(title) = self.title {
262 | notification.summary(&title);
263 | }
264 | if let Some(icon) = app_.default_window_icon() {
265 | notification.icon(icon.rgba().to_vec(), icon.width(), icon.height());
266 | }
267 | let _ = notification.show();
268 | });
269 |
270 | Ok(())
271 | }
272 | }
273 | }
274 |
--------------------------------------------------------------------------------
/src/error.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | use serde::{ser::Serializer, Serialize};
6 |
7 | pub type Result = std::result::Result;
8 |
9 | #[derive(Debug, thiserror::Error)]
10 | pub enum Error {
11 | #[error(transparent)]
12 | Io(#[from] std::io::Error),
13 | #[cfg(mobile)]
14 | #[error(transparent)]
15 | PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),
16 | }
17 |
18 | impl Serialize for Error {
19 | fn serialize(&self, serializer: S) -> std::result::Result
20 | where
21 | S: Serializer,
22 | {
23 | serializer.serialize_str(self.to_string().as_ref())
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/init-iife.js:
--------------------------------------------------------------------------------
1 | !function(){"use strict";async function i(i,n={},t){return window.__TAURI_INTERNALS__.invoke(i,n,t)}"function"==typeof SuppressedError&&SuppressedError,function(){let n=!1,t="default";function o(i){n=!0,window.Notification.permission=i,n=!1}window.Notification=function(n,t){const o=t||{};!async function(n){"object"==typeof n&&Object.freeze(n),await i("plugin:notification|notify",{options:"string"==typeof n?{title:n}:n})}(Object.assign(o,{title:n}))},window.Notification.requestPermission=async function(){return await i("plugin:notification|request_permission").then((i=>(o("prompt"===i||"prompt-with-rationale"===i?"default":i),i)))},Object.defineProperty(window.Notification,"permission",{enumerable:!0,get:()=>t,set:i=>{if(!n)throw new Error("Readonly property");t=i}}),async function(){return"default"!==window.Notification.permission||__TEMPLATE_windows__?await Promise.resolve("granted"===window.Notification.permission):await i("plugin:notification|is_permission_granted")}().then((function(i){o(null===i?"default":i?"granted":"denied")}))}()}();
2 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | //! Send message notifications (brief auto-expiring OS window element) to your user. Can also be used with the Notification Web API.
6 |
7 | #![doc(
8 | html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
9 | html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
10 | )]
11 |
12 | use serde::Serialize;
13 | #[cfg(mobile)]
14 | use tauri::plugin::PluginHandle;
15 | #[cfg(desktop)]
16 | use tauri::AppHandle;
17 | use tauri::{
18 | plugin::{Builder, TauriPlugin},
19 | Manager, Runtime,
20 | };
21 |
22 | pub use models::*;
23 | pub use tauri::plugin::PermissionState;
24 |
25 | #[cfg(desktop)]
26 | mod desktop;
27 | #[cfg(mobile)]
28 | mod mobile;
29 |
30 | mod commands;
31 | mod error;
32 | mod models;
33 |
34 | pub use error::{Error, Result};
35 |
36 | #[cfg(desktop)]
37 | pub use desktop::Notification;
38 | #[cfg(mobile)]
39 | pub use mobile::Notification;
40 |
41 | /// The notification builder.
42 | #[derive(Debug)]
43 | pub struct NotificationBuilder {
44 | #[cfg(desktop)]
45 | app: AppHandle,
46 | #[cfg(mobile)]
47 | handle: PluginHandle,
48 | pub(crate) data: NotificationData,
49 | }
50 |
51 | impl NotificationBuilder {
52 | #[cfg(desktop)]
53 | fn new(app: AppHandle) -> Self {
54 | Self {
55 | app,
56 | data: Default::default(),
57 | }
58 | }
59 |
60 | #[cfg(mobile)]
61 | fn new(handle: PluginHandle) -> Self {
62 | Self {
63 | handle,
64 | data: Default::default(),
65 | }
66 | }
67 |
68 | /// Sets the notification identifier.
69 | pub fn id(mut self, id: i32) -> Self {
70 | self.data.id = id;
71 | self
72 | }
73 |
74 | /// Identifier of the {@link Channel} that deliveres this notification.
75 | ///
76 | /// If the channel does not exist, the notification won't fire.
77 | /// Make sure the channel exists with {@link listChannels} and {@link createChannel}.
78 | pub fn channel_id(mut self, id: impl Into) -> Self {
79 | self.data.channel_id.replace(id.into());
80 | self
81 | }
82 |
83 | /// Sets the notification title.
84 | pub fn title(mut self, title: impl Into) -> Self {
85 | self.data.title.replace(title.into());
86 | self
87 | }
88 |
89 | /// Sets the notification body.
90 | pub fn body(mut self, body: impl Into) -> Self {
91 | self.data.body.replace(body.into());
92 | self
93 | }
94 |
95 | /// Schedule this notification to fire on a later time or a fixed interval.
96 | pub fn schedule(mut self, schedule: Schedule) -> Self {
97 | self.data.schedule.replace(schedule);
98 | self
99 | }
100 |
101 | /// Multiline text.
102 | /// Changes the notification style to big text.
103 | /// Cannot be used with `inboxLines`.
104 | pub fn large_body(mut self, large_body: impl Into) -> Self {
105 | self.data.large_body.replace(large_body.into());
106 | self
107 | }
108 |
109 | /// Detail text for the notification with `largeBody`, `inboxLines` or `groupSummary`.
110 | pub fn summary(mut self, summary: impl Into) -> Self {
111 | self.data.summary.replace(summary.into());
112 | self
113 | }
114 |
115 | /// Defines an action type for this notification.
116 | pub fn action_type_id(mut self, action_type_id: impl Into) -> Self {
117 | self.data.action_type_id.replace(action_type_id.into());
118 | self
119 | }
120 |
121 | /// Identifier used to group multiple notifications.
122 | ///
123 | ///
124 | pub fn group(mut self, group: impl Into) -> Self {
125 | self.data.group.replace(group.into());
126 | self
127 | }
128 |
129 | /// Instructs the system that this notification is the summary of a group on Android.
130 | pub fn group_summary(mut self) -> Self {
131 | self.data.group_summary = true;
132 | self
133 | }
134 |
135 | /// The sound resource name. Only available on mobile.
136 | pub fn sound(mut self, sound: impl Into) -> Self {
137 | self.data.sound.replace(sound.into());
138 | self
139 | }
140 |
141 | /// Append an inbox line to the notification.
142 | /// Changes the notification style to inbox.
143 | /// Cannot be used with `largeBody`.
144 | ///
145 | /// Only supports up to 5 lines.
146 | pub fn inbox_line(mut self, line: impl Into) -> Self {
147 | self.data.inbox_lines.push(line.into());
148 | self
149 | }
150 |
151 | /// Notification icon.
152 | ///
153 | /// On Android the icon must be placed in the app's `res/drawable` folder.
154 | pub fn icon(mut self, icon: impl Into) -> Self {
155 | self.data.icon.replace(icon.into());
156 | self
157 | }
158 |
159 | /// Notification large icon (Android).
160 | ///
161 | /// The icon must be placed in the app's `res/drawable` folder.
162 | pub fn large_icon(mut self, large_icon: impl Into) -> Self {
163 | self.data.large_icon.replace(large_icon.into());
164 | self
165 | }
166 |
167 | /// Icon color on Android.
168 | pub fn icon_color(mut self, icon_color: impl Into) -> Self {
169 | self.data.icon_color.replace(icon_color.into());
170 | self
171 | }
172 |
173 | /// Append an attachment to the notification.
174 | pub fn attachment(mut self, attachment: Attachment) -> Self {
175 | self.data.attachments.push(attachment);
176 | self
177 | }
178 |
179 | /// Adds an extra payload to store in the notification.
180 | pub fn extra(mut self, key: impl Into, value: impl Serialize) -> Self {
181 | self.data
182 | .extra
183 | .insert(key.into(), serde_json::to_value(value).unwrap());
184 | self
185 | }
186 |
187 | /// If true, the notification cannot be dismissed by the user on Android.
188 | ///
189 | /// An application service must manage the dismissal of the notification.
190 | /// It is typically used to indicate a background task that is pending (e.g. a file download)
191 | /// or the user is engaged with (e.g. playing music).
192 | pub fn ongoing(mut self) -> Self {
193 | self.data.ongoing = true;
194 | self
195 | }
196 |
197 | /// Automatically cancel the notification when the user clicks on it.
198 | pub fn auto_cancel(mut self) -> Self {
199 | self.data.auto_cancel = true;
200 | self
201 | }
202 |
203 | /// Changes the notification presentation to be silent on iOS (no badge, no sound, not listed).
204 | pub fn silent(mut self) -> Self {
205 | self.data.silent = true;
206 | self
207 | }
208 | }
209 |
210 | /// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the notification APIs.
211 | pub trait NotificationExt {
212 | fn notification(&self) -> &Notification;
213 | }
214 |
215 | impl> crate::NotificationExt for T {
216 | fn notification(&self) -> &Notification {
217 | self.state::>().inner()
218 | }
219 | }
220 |
221 | /// Initializes the plugin.
222 | pub fn init() -> TauriPlugin {
223 | Builder::new("notification")
224 | .invoke_handler(tauri::generate_handler![
225 | commands::notify,
226 | commands::request_permission,
227 | commands::is_permission_granted
228 | ])
229 | .js_init_script(include_str!("init-iife.js").replace(
230 | "__TEMPLATE_windows__",
231 | if cfg!(windows) { "true" } else { "false" },
232 | ))
233 | .setup(|app, api| {
234 | #[cfg(mobile)]
235 | let notification = mobile::init(app, api)?;
236 | #[cfg(desktop)]
237 | let notification = desktop::init(app, api)?;
238 | app.manage(notification);
239 | Ok(())
240 | })
241 | .build()
242 | }
243 |
--------------------------------------------------------------------------------
/src/mobile.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | use serde::{de::DeserializeOwned, Deserialize};
6 | use tauri::{
7 | plugin::{PermissionState, PluginApi, PluginHandle},
8 | AppHandle, Runtime,
9 | };
10 |
11 | use crate::models::*;
12 |
13 | use std::collections::HashMap;
14 |
15 | #[cfg(target_os = "android")]
16 | const PLUGIN_IDENTIFIER: &str = "app.tauri.notification";
17 |
18 | #[cfg(target_os = "ios")]
19 | tauri::ios_plugin_binding!(init_plugin_notification);
20 |
21 | // initializes the Kotlin or Swift plugin classes
22 | pub fn init(
23 | _app: &AppHandle,
24 | api: PluginApi,
25 | ) -> crate::Result> {
26 | #[cfg(target_os = "android")]
27 | let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "NotificationPlugin")?;
28 | #[cfg(target_os = "ios")]
29 | let handle = api.register_ios_plugin(init_plugin_notification)?;
30 | Ok(Notification(handle))
31 | }
32 |
33 | impl crate::NotificationBuilder {
34 | pub fn show(self) -> crate::Result<()> {
35 | self.handle
36 | .run_mobile_plugin::("show", self.data)
37 | .map(|_| ())
38 | .map_err(Into::into)
39 | }
40 | }
41 |
42 | /// Access to the notification APIs.
43 | ///
44 | /// You can get an instance of this type via [`NotificationExt`](crate::NotificationExt)
45 | pub struct Notification(PluginHandle);
46 |
47 | impl Notification {
48 | pub fn builder(&self) -> crate::NotificationBuilder {
49 | crate::NotificationBuilder::new(self.0.clone())
50 | }
51 |
52 | pub fn request_permission(&self) -> crate::Result {
53 | self.0
54 | .run_mobile_plugin::("requestPermissions", ())
55 | .map(|r| r.permission_state)
56 | .map_err(Into::into)
57 | }
58 |
59 | pub fn permission_state(&self) -> crate::Result {
60 | self.0
61 | .run_mobile_plugin::("checkPermissions", ())
62 | .map(|r| r.permission_state)
63 | .map_err(Into::into)
64 | }
65 |
66 | pub fn register_action_types(&self, types: Vec) -> crate::Result<()> {
67 | let mut args = HashMap::new();
68 | args.insert("types", types);
69 | self.0
70 | .run_mobile_plugin("registerActionTypes", args)
71 | .map_err(Into::into)
72 | }
73 |
74 | pub fn remove_active(&self, notifications: Vec) -> crate::Result<()> {
75 | let mut args = HashMap::new();
76 | args.insert(
77 | "notifications",
78 | notifications
79 | .into_iter()
80 | .map(|id| {
81 | let mut notification = HashMap::new();
82 | notification.insert("id", id);
83 | notification
84 | })
85 | .collect::>>(),
86 | );
87 | self.0
88 | .run_mobile_plugin("removeActive", args)
89 | .map_err(Into::into)
90 | }
91 |
92 | pub fn active(&self) -> crate::Result> {
93 | self.0
94 | .run_mobile_plugin("getActive", ())
95 | .map_err(Into::into)
96 | }
97 |
98 | pub fn remove_all_active(&self) -> crate::Result<()> {
99 | self.0
100 | .run_mobile_plugin("removeActive", ())
101 | .map_err(Into::into)
102 | }
103 |
104 | pub fn pending(&self) -> crate::Result> {
105 | self.0
106 | .run_mobile_plugin("getPending", ())
107 | .map_err(Into::into)
108 | }
109 |
110 | /// Cancel pending notifications.
111 | pub fn cancel(&self, notifications: Vec) -> crate::Result<()> {
112 | let mut args = HashMap::new();
113 | args.insert("notifications", notifications);
114 | self.0.run_mobile_plugin("cancel", args).map_err(Into::into)
115 | }
116 |
117 | /// Cancel all pending notifications.
118 | pub fn cancel_all(&self) -> crate::Result<()> {
119 | self.0.run_mobile_plugin("cancel", ()).map_err(Into::into)
120 | }
121 |
122 | #[cfg(target_os = "android")]
123 | pub fn create_channel(&self, channel: Channel) -> crate::Result<()> {
124 | self.0
125 | .run_mobile_plugin("createChannel", channel)
126 | .map_err(Into::into)
127 | }
128 |
129 | #[cfg(target_os = "android")]
130 | pub fn delete_channel(&self, id: impl Into) -> crate::Result<()> {
131 | let mut args = HashMap::new();
132 | args.insert("id", id.into());
133 | self.0
134 | .run_mobile_plugin("deleteChannel", args)
135 | .map_err(Into::into)
136 | }
137 |
138 | #[cfg(target_os = "android")]
139 | pub fn list_channels(&self) -> crate::Result> {
140 | self.0
141 | .run_mobile_plugin("listChannels", ())
142 | .map_err(Into::into)
143 | }
144 | }
145 |
146 | #[derive(Deserialize)]
147 | #[serde(rename_all = "camelCase")]
148 | struct PermissionResponse {
149 | permission_state: PermissionState,
150 | }
151 |
--------------------------------------------------------------------------------
/src/models.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2 | // SPDX-License-Identifier: Apache-2.0
3 | // SPDX-License-Identifier: MIT
4 |
5 | use std::{collections::HashMap, fmt::Display};
6 |
7 | use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer};
8 |
9 | use url::Url;
10 |
11 | #[derive(Debug, Serialize, Deserialize)]
12 | #[serde(rename_all = "camelCase")]
13 | pub struct Attachment {
14 | id: String,
15 | url: Url,
16 | }
17 |
18 | impl Attachment {
19 | pub fn new(id: impl Into, url: Url) -> Self {
20 | Self { id: id.into(), url }
21 | }
22 | }
23 |
24 | #[derive(Debug, Default, Serialize, Deserialize)]
25 | #[serde(rename_all = "camelCase")]
26 | pub struct ScheduleInterval {
27 | pub year: Option,
28 | pub month: Option,
29 | pub day: Option,
30 | pub weekday: Option,
31 | pub hour: Option,
32 | pub minute: Option,
33 | pub second: Option,
34 | }
35 |
36 | #[derive(Debug)]
37 | pub enum ScheduleEvery {
38 | Year,
39 | Month,
40 | TwoWeeks,
41 | Week,
42 | Day,
43 | Hour,
44 | Minute,
45 | Second,
46 | }
47 |
48 | impl Display for ScheduleEvery {
49 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 | write!(
51 | f,
52 | "{}",
53 | match self {
54 | Self::Year => "year",
55 | Self::Month => "month",
56 | Self::TwoWeeks => "twoWeeks",
57 | Self::Week => "week",
58 | Self::Day => "day",
59 | Self::Hour => "hour",
60 | Self::Minute => "minute",
61 | Self::Second => "second",
62 | }
63 | )
64 | }
65 | }
66 |
67 | impl Serialize for ScheduleEvery {
68 | fn serialize(&self, serializer: S) -> std::result::Result
69 | where
70 | S: Serializer,
71 | {
72 | serializer.serialize_str(self.to_string().as_ref())
73 | }
74 | }
75 |
76 | impl<'de> Deserialize<'de> for ScheduleEvery {
77 | fn deserialize(deserializer: D) -> std::result::Result
78 | where
79 | D: Deserializer<'de>,
80 | {
81 | let s = String::deserialize(deserializer)?;
82 | match s.to_lowercase().as_str() {
83 | "year" => Ok(Self::Year),
84 | "month" => Ok(Self::Month),
85 | "twoweeks" => Ok(Self::TwoWeeks),
86 | "week" => Ok(Self::Week),
87 | "day" => Ok(Self::Day),
88 | "hour" => Ok(Self::Hour),
89 | "minute" => Ok(Self::Minute),
90 | "second" => Ok(Self::Second),
91 | _ => Err(DeError::custom(format!("unknown every kind '{s}'"))),
92 | }
93 | }
94 | }
95 |
96 | #[derive(Debug, Serialize, Deserialize)]
97 | #[serde(rename_all = "camelCase")]
98 | pub enum Schedule {
99 | #[serde(rename_all = "camelCase")]
100 | At {
101 | #[serde(
102 | serialize_with = "iso8601::serialize",
103 | deserialize_with = "time::serde::iso8601::deserialize"
104 | )]
105 | date: time::OffsetDateTime,
106 | #[serde(default)]
107 | repeating: bool,
108 | #[serde(default)]
109 | allow_while_idle: bool,
110 | },
111 | #[serde(rename_all = "camelCase")]
112 | Interval {
113 | interval: ScheduleInterval,
114 | #[serde(default)]
115 | allow_while_idle: bool,
116 | },
117 | #[serde(rename_all = "camelCase")]
118 | Every {
119 | interval: ScheduleEvery,
120 | count: u8,
121 | #[serde(default)]
122 | allow_while_idle: bool,
123 | },
124 | }
125 |
126 | // custom ISO-8601 serialization that does not use 6 digits for years.
127 | mod iso8601 {
128 | use serde::{ser::Error as _, Serialize, Serializer};
129 | use time::{
130 | format_description::well_known::iso8601::{Config, EncodedConfig},
131 | format_description::well_known::Iso8601,
132 | OffsetDateTime,
133 | };
134 |
135 | const SERDE_CONFIG: EncodedConfig = Config::DEFAULT.encode();
136 |
137 | pub fn serialize(
138 | datetime: &OffsetDateTime,
139 | serializer: S,
140 | ) -> Result {
141 | datetime
142 | .format(&Iso8601::)
143 | .map_err(S::Error::custom)?
144 | .serialize(serializer)
145 | }
146 | }
147 |
148 | #[derive(Debug, Serialize, Deserialize)]
149 | #[serde(rename_all = "camelCase")]
150 | pub struct NotificationData {
151 | #[serde(default = "default_id")]
152 | pub(crate) id: i32,
153 | pub(crate) channel_id: Option,
154 | pub(crate) title: Option,
155 | pub(crate) body: Option,
156 | pub(crate) schedule: Option,
157 | pub(crate) large_body: Option,
158 | pub(crate) summary: Option,
159 | pub(crate) action_type_id: Option,
160 | pub(crate) group: Option,
161 | #[serde(default)]
162 | pub(crate) group_summary: bool,
163 | pub(crate) sound: Option,
164 | #[serde(default)]
165 | pub(crate) inbox_lines: Vec,
166 | pub(crate) icon: Option,
167 | pub(crate) large_icon: Option,
168 | pub(crate) icon_color: Option,
169 | #[serde(default)]
170 | pub(crate) attachments: Vec,
171 | #[serde(default)]
172 | pub(crate) extra: HashMap,
173 | #[serde(default)]
174 | pub(crate) ongoing: bool,
175 | #[serde(default)]
176 | pub(crate) auto_cancel: bool,
177 | #[serde(default)]
178 | pub(crate) silent: bool,
179 | }
180 |
181 | fn default_id() -> i32 {
182 | rand::random()
183 | }
184 |
185 | impl Default for NotificationData {
186 | fn default() -> Self {
187 | Self {
188 | id: default_id(),
189 | channel_id: None,
190 | title: None,
191 | body: None,
192 | schedule: None,
193 | large_body: None,
194 | summary: None,
195 | action_type_id: None,
196 | group: None,
197 | group_summary: false,
198 | sound: None,
199 | inbox_lines: Vec::new(),
200 | icon: None,
201 | large_icon: None,
202 | icon_color: None,
203 | attachments: Vec::new(),
204 | extra: Default::default(),
205 | ongoing: false,
206 | auto_cancel: false,
207 | silent: false,
208 | }
209 | }
210 | }
211 |
212 | #[derive(Debug, Deserialize)]
213 | #[serde(rename_all = "camelCase")]
214 | pub struct PendingNotification {
215 | id: i32,
216 | title: Option,
217 | body: Option,
218 | schedule: Schedule,
219 | }
220 |
221 | impl PendingNotification {
222 | pub fn id(&self) -> i32 {
223 | self.id
224 | }
225 |
226 | pub fn title(&self) -> Option<&str> {
227 | self.title.as_deref()
228 | }
229 |
230 | pub fn body(&self) -> Option<&str> {
231 | self.body.as_deref()
232 | }
233 |
234 | pub fn schedule(&self) -> &Schedule {
235 | &self.schedule
236 | }
237 | }
238 |
239 | #[derive(Debug, Deserialize)]
240 | #[serde(rename_all = "camelCase")]
241 | pub struct ActiveNotification {
242 | id: i32,
243 | tag: Option,
244 | title: Option,
245 | body: Option,
246 | group: Option,
247 | #[serde(default)]
248 | group_summary: bool,
249 | #[serde(default)]
250 | data: HashMap,
251 | #[serde(default)]
252 | extra: HashMap,
253 | #[serde(default)]
254 | attachments: Vec,
255 | action_type_id: Option,
256 | schedule: Option,
257 | sound: Option,
258 | }
259 |
260 | impl ActiveNotification {
261 | pub fn id(&self) -> i32 {
262 | self.id
263 | }
264 |
265 | pub fn tag(&self) -> Option<&str> {
266 | self.tag.as_deref()
267 | }
268 |
269 | pub fn title(&self) -> Option<&str> {
270 | self.title.as_deref()
271 | }
272 |
273 | pub fn body(&self) -> Option<&str> {
274 | self.body.as_deref()
275 | }
276 |
277 | pub fn group(&self) -> Option<&str> {
278 | self.group.as_deref()
279 | }
280 |
281 | pub fn group_summary(&self) -> bool {
282 | self.group_summary
283 | }
284 |
285 | pub fn data(&self) -> &HashMap {
286 | &self.data
287 | }
288 |
289 | pub fn extra(&self) -> &HashMap {
290 | &self.extra
291 | }
292 |
293 | pub fn attachments(&self) -> &[Attachment] {
294 | &self.attachments
295 | }
296 |
297 | pub fn action_type_id(&self) -> Option<&str> {
298 | self.action_type_id.as_deref()
299 | }
300 |
301 | pub fn schedule(&self) -> Option<&Schedule> {
302 | self.schedule.as_ref()
303 | }
304 |
305 | pub fn sound(&self) -> Option<&str> {
306 | self.sound.as_deref()
307 | }
308 | }
309 |
310 | #[cfg(mobile)]
311 | #[derive(Debug, Serialize)]
312 | #[serde(rename_all = "camelCase")]
313 | pub struct ActionType {
314 | id: String,
315 | actions: Vec,
316 | hidden_previews_body_placeholder: Option,
317 | custom_dismiss_action: bool,
318 | allow_in_car_play: bool,
319 | hidden_previews_show_title: bool,
320 | hidden_previews_show_subtitle: bool,
321 | }
322 |
323 | #[cfg(mobile)]
324 | #[derive(Debug, Serialize)]
325 | #[serde(rename_all = "camelCase")]
326 | pub struct Action {
327 | id: String,
328 | title: String,
329 | requires_authentication: bool,
330 | foreground: bool,
331 | destructive: bool,
332 | input: bool,
333 | input_button_title: Option,
334 | input_placeholder: Option,
335 | }
336 |
337 | #[cfg(target_os = "android")]
338 | pub use android::*;
339 |
340 | #[cfg(target_os = "android")]
341 | mod android {
342 | use serde::{Deserialize, Serialize};
343 | use serde_repr::{Deserialize_repr, Serialize_repr};
344 |
345 | #[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr)]
346 | #[repr(u8)]
347 | pub enum Importance {
348 | None = 0,
349 | Min = 1,
350 | Low = 2,
351 | Default = 3,
352 | High = 4,
353 | }
354 |
355 | impl Default for Importance {
356 | fn default() -> Self {
357 | Self::Default
358 | }
359 | }
360 |
361 | #[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr)]
362 | #[repr(i8)]
363 | pub enum Visibility {
364 | Secret = -1,
365 | Private = 0,
366 | Public = 1,
367 | }
368 |
369 | #[derive(Debug, Serialize, Deserialize)]
370 | #[serde(rename_all = "camelCase")]
371 | pub struct Channel {
372 | id: String,
373 | name: String,
374 | description: Option,
375 | sound: Option,
376 | lights: bool,
377 | light_color: Option,
378 | vibration: bool,
379 | importance: Importance,
380 | visibility: Option,
381 | }
382 |
383 | #[derive(Debug)]
384 | pub struct ChannelBuilder(Channel);
385 |
386 | impl Channel {
387 | pub fn builder(id: impl Into, name: impl Into) -> ChannelBuilder {
388 | ChannelBuilder(Self {
389 | id: id.into(),
390 | name: name.into(),
391 | description: None,
392 | sound: None,
393 | lights: false,
394 | light_color: None,
395 | vibration: false,
396 | importance: Default::default(),
397 | visibility: None,
398 | })
399 | }
400 |
401 | pub fn id(&self) -> &str {
402 | &self.id
403 | }
404 |
405 | pub fn name(&self) -> &str {
406 | &self.name
407 | }
408 |
409 | pub fn description(&self) -> Option<&str> {
410 | self.description.as_deref()
411 | }
412 |
413 | pub fn sound(&self) -> Option<&str> {
414 | self.sound.as_deref()
415 | }
416 |
417 | pub fn lights(&self) -> bool {
418 | self.lights
419 | }
420 |
421 | pub fn light_color(&self) -> Option<&str> {
422 | self.light_color.as_deref()
423 | }
424 |
425 | pub fn vibration(&self) -> bool {
426 | self.vibration
427 | }
428 |
429 | pub fn importance(&self) -> Importance {
430 | self.importance
431 | }
432 |
433 | pub fn visibility(&self) -> Option {
434 | self.visibility
435 | }
436 | }
437 |
438 | impl ChannelBuilder {
439 | pub fn description(mut self, description: impl Into) -> Self {
440 | self.0.description.replace(description.into());
441 | self
442 | }
443 |
444 | pub fn sound(mut self, sound: impl Into) -> Self {
445 | self.0.sound.replace(sound.into());
446 | self
447 | }
448 |
449 | pub fn lights(mut self, lights: bool) -> Self {
450 | self.0.lights = lights;
451 | self
452 | }
453 |
454 | pub fn light_color(mut self, color: impl Into) -> Self {
455 | self.0.light_color.replace(color.into());
456 | self
457 | }
458 |
459 | pub fn vibration(mut self, vibration: bool) -> Self {
460 | self.0.vibration = vibration;
461 | self
462 | }
463 |
464 | pub fn importance(mut self, importance: Importance) -> Self {
465 | self.0.importance = importance;
466 | self
467 | }
468 |
469 | pub fn visibility(mut self, visibility: Visibility) -> Self {
470 | self.0.visibility.replace(visibility);
471 | self
472 | }
473 |
474 | pub fn build(self) -> Channel {
475 | self.0
476 | }
477 | }
478 | }
479 |
--------------------------------------------------------------------------------
/test/tauri.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "identifier": "app.tauri.example",
3 | "build": {
4 | "frontendDist": ".",
5 | "devUrl": "http://localhost:4000"
6 | },
7 | "app": {
8 | "windows": [
9 | {
10 | "title": "Tauri App"
11 | }
12 | ],
13 | "security": {
14 | "csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: http://tauri.localhost 'unsafe-eval' 'unsafe-inline' 'self'"
15 | }
16 | },
17 | "bundle": {
18 | "active": true,
19 | "icon": ["../../../examples/api/src-tauri/icons/icon.png"]
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "include": ["guest-js/*.ts"]
4 | }
5 |
--------------------------------------------------------------------------------