├── 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 | ![plugin-notification](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/notification/banner.png) 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 | 121 | 122 | 123 |
117 | 118 | CrabNebula 119 | 120 |
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 | 36 | 37 | 38 | 39 | 40 | 41 | 46 | 51 | 52 | 53 | 54 | 59 | 64 | 65 | 66 | 67 | 72 | 77 | 78 | 79 | 80 | 85 | 90 | 91 | 92 | 93 | 98 | 103 | 104 | 105 | 106 | 111 | 116 | 117 | 118 | 119 | 124 | 129 | 130 | 131 | 132 | 137 | 142 | 143 | 144 | 145 | 150 | 155 | 156 | 157 | 158 | 163 | 168 | 169 | 170 | 171 | 176 | 181 | 182 | 183 | 184 | 189 | 194 | 195 | 196 | 197 | 202 | 207 | 208 | 209 | 210 | 215 | 220 | 221 | 222 | 223 | 228 | 233 | 234 | 235 | 236 | 241 | 246 | 247 | 248 | 249 | 254 | 259 | 260 | 261 | 262 | 267 | 272 | 273 | 274 | 275 | 280 | 285 | 286 | 287 | 288 | 293 | 298 | 299 | 300 | 301 | 306 | 311 | 312 | 313 | 314 | 319 | 324 | 325 | 326 | 327 | 332 | 337 | 338 | 339 | 340 | 345 | 350 | 351 | 352 | 353 | 358 | 363 | 364 | 365 | 366 | 371 | 376 | 377 | 378 | 379 | 384 | 389 | 390 | 391 | 392 | 397 | 402 | 403 | 404 | 405 | 410 | 415 | 416 | 417 | 418 | 423 | 428 | 429 | 430 | 431 | 436 | 441 | 442 | 443 | 444 | 449 | 454 | 455 |
IdentifierDescription
42 | 43 | `notification:allow-batch` 44 | 45 | 47 | 48 | Enables the batch command without any pre-configured scope. 49 | 50 |
55 | 56 | `notification:deny-batch` 57 | 58 | 60 | 61 | Denies the batch command without any pre-configured scope. 62 | 63 |
68 | 69 | `notification:allow-cancel` 70 | 71 | 73 | 74 | Enables the cancel command without any pre-configured scope. 75 | 76 |
81 | 82 | `notification:deny-cancel` 83 | 84 | 86 | 87 | Denies the cancel command without any pre-configured scope. 88 | 89 |
94 | 95 | `notification:allow-check-permissions` 96 | 97 | 99 | 100 | Enables the check_permissions command without any pre-configured scope. 101 | 102 |
107 | 108 | `notification:deny-check-permissions` 109 | 110 | 112 | 113 | Denies the check_permissions command without any pre-configured scope. 114 | 115 |
120 | 121 | `notification:allow-create-channel` 122 | 123 | 125 | 126 | Enables the create_channel command without any pre-configured scope. 127 | 128 |
133 | 134 | `notification:deny-create-channel` 135 | 136 | 138 | 139 | Denies the create_channel command without any pre-configured scope. 140 | 141 |
146 | 147 | `notification:allow-delete-channel` 148 | 149 | 151 | 152 | Enables the delete_channel command without any pre-configured scope. 153 | 154 |
159 | 160 | `notification:deny-delete-channel` 161 | 162 | 164 | 165 | Denies the delete_channel command without any pre-configured scope. 166 | 167 |
172 | 173 | `notification:allow-get-active` 174 | 175 | 177 | 178 | Enables the get_active command without any pre-configured scope. 179 | 180 |
185 | 186 | `notification:deny-get-active` 187 | 188 | 190 | 191 | Denies the get_active command without any pre-configured scope. 192 | 193 |
198 | 199 | `notification:allow-get-pending` 200 | 201 | 203 | 204 | Enables the get_pending command without any pre-configured scope. 205 | 206 |
211 | 212 | `notification:deny-get-pending` 213 | 214 | 216 | 217 | Denies the get_pending command without any pre-configured scope. 218 | 219 |
224 | 225 | `notification:allow-is-permission-granted` 226 | 227 | 229 | 230 | Enables the is_permission_granted command without any pre-configured scope. 231 | 232 |
237 | 238 | `notification:deny-is-permission-granted` 239 | 240 | 242 | 243 | Denies the is_permission_granted command without any pre-configured scope. 244 | 245 |
250 | 251 | `notification:allow-list-channels` 252 | 253 | 255 | 256 | Enables the list_channels command without any pre-configured scope. 257 | 258 |
263 | 264 | `notification:deny-list-channels` 265 | 266 | 268 | 269 | Denies the list_channels command without any pre-configured scope. 270 | 271 |
276 | 277 | `notification:allow-notify` 278 | 279 | 281 | 282 | Enables the notify command without any pre-configured scope. 283 | 284 |
289 | 290 | `notification:deny-notify` 291 | 292 | 294 | 295 | Denies the notify command without any pre-configured scope. 296 | 297 |
302 | 303 | `notification:allow-permission-state` 304 | 305 | 307 | 308 | Enables the permission_state command without any pre-configured scope. 309 | 310 |
315 | 316 | `notification:deny-permission-state` 317 | 318 | 320 | 321 | Denies the permission_state command without any pre-configured scope. 322 | 323 |
328 | 329 | `notification:allow-register-action-types` 330 | 331 | 333 | 334 | Enables the register_action_types command without any pre-configured scope. 335 | 336 |
341 | 342 | `notification:deny-register-action-types` 343 | 344 | 346 | 347 | Denies the register_action_types command without any pre-configured scope. 348 | 349 |
354 | 355 | `notification:allow-register-listener` 356 | 357 | 359 | 360 | Enables the register_listener command without any pre-configured scope. 361 | 362 |
367 | 368 | `notification:deny-register-listener` 369 | 370 | 372 | 373 | Denies the register_listener command without any pre-configured scope. 374 | 375 |
380 | 381 | `notification:allow-remove-active` 382 | 383 | 385 | 386 | Enables the remove_active command without any pre-configured scope. 387 | 388 |
393 | 394 | `notification:deny-remove-active` 395 | 396 | 398 | 399 | Denies the remove_active command without any pre-configured scope. 400 | 401 |
406 | 407 | `notification:allow-request-permission` 408 | 409 | 411 | 412 | Enables the request_permission command without any pre-configured scope. 413 | 414 |
419 | 420 | `notification:deny-request-permission` 421 | 422 | 424 | 425 | Denies the request_permission command without any pre-configured scope. 426 | 427 |
432 | 433 | `notification:allow-show` 434 | 435 | 437 | 438 | Enables the show command without any pre-configured scope. 439 | 440 |
445 | 446 | `notification:deny-show` 447 | 448 | 450 | 451 | Denies the show command without any pre-configured scope. 452 | 453 |
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 | --------------------------------------------------------------------------------