From fe79adb5c7febd0e912efb5581264d671709fbb0 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira <118899497+lucasfernog-crabnebula@users.noreply.github.com> Date: Tue, 19 Dec 2023 10:50:31 -0300 Subject: [PATCH] feat(mobile): add NFC plugin (#830) * feat: scaffold NFC plugin, initial iOS code * adjust script paths (api example) * update entitlements & plist * update class name * update api * sketch api, remove desktop * update response data * add write fn * remove commands * fixes for write mode * check nfc state before using the APIs * fix(example): downgrade internal-ip to v7 * feat: typed iOS arguments * update swift requirement * android updates * update tauri * fix icon * update example * fix build * fix notification example * fix clipboard * fix ios notification build * fix info.plist * update tauri * add change file * fmt * update to new args class syntax :( [skip ci] * add lang code handling in RTD_TEXT helper (written payload is broken without it) * update nfc to latest tauri. use tauri from git * update lockfile * android: add initial nfc writer implementation * check sdk version for pendingintent flag * quicksaving basic ndef reading that doesn't crash * add basic ndef reading (android) * small cleanup * change pending action type * validate available state * gradle 8.0.0 * use session class * implement keep session alive * fix notification crash?? * remove dox feature, fix breaking changes * update dependencies * fix shell tests [skip ci] * fmt [skip ci] * type safe args * scan kind options * commit .idea files * update api * update example * fix app check * alertmessage options for iOS * default to tag on example * fix(ios): always close session on write, remove keepsessionalive option * add kind input to write options * empty records if message not found * fill tag metadata for ndef read * add contributors * update vite * covector setup * tauri/dox removed * docs and examples * fmt [skip ci] --------- Co-authored-by: FabianLars-crabnebula Co-authored-by: Lucas Nogueira Co-authored-by: Lucas Nogueira --- .changes/config.json | 9 + .changes/nfc-initial-release.md | 6 + Cargo.lock | 14 + examples/api/package.json | 1 + examples/api/src-tauri/Cargo.toml | 1 + .../src-tauri/gen/android/.idea/.gitignore | 3 + .../src-tauri/gen/android/.idea/compiler.xml | 6 + .../src-tauri/gen/android/.idea/gradle.xml | 27 + .../gen/android/.idea/jarRepositories.xml | 25 + .../src-tauri/gen/android/.idea/kotlinc.xml | 6 + .../api/src-tauri/gen/android/.idea/misc.xml | 9 + .../api/src-tauri/gen/android/.idea/vcs.xml | 6 + .../android/app/src/main/AndroidManifest.xml | 20 + .../xcshareddata/xcschemes/api_iOS.xcscheme | 26 +- .../src-tauri/gen/apple/api_iOS/Info.plist | 2 + .../gen/apple/api_iOS/api_iOS.entitlements | 8 +- examples/api/src-tauri/src/lib.rs | 1 + examples/api/src/App.svelte | 8 +- examples/api/src/views/Nfc.svelte | 126 +++++ plugins/deep-link/examples/app/package.json | 2 +- plugins/nfc/.gitignore | 1 + plugins/nfc/Cargo.toml | 22 + plugins/nfc/LICENSE.spdx | 20 + plugins/nfc/LICENSE_APACHE-2.0 | 177 ++++++ plugins/nfc/LICENSE_MIT | 21 + plugins/nfc/README.md | 113 ++++ plugins/nfc/android/.gitignore | 2 + plugins/nfc/android/build.gradle.kts | 46 ++ plugins/nfc/android/proguard-rules.pro | 21 + plugins/nfc/android/settings.gradle | 2 + .../java/ExampleInstrumentedTest.kt | 28 + .../nfc/android/src/main/AndroidManifest.xml | 9 + .../nfc/android/src/main/java/NfcPlugin.kt | 521 ++++++++++++++++++ .../src/main/res/xml/nfc_tech_filter.xml | 13 + .../android/src/test/java/ExampleUnitTest.kt | 21 + plugins/nfc/build.rs | 43 ++ plugins/nfc/contributors/crabnebula.svg | 31 ++ plugins/nfc/contributors/impierce.svg | 21 + plugins/nfc/guest-js/index.ts | 272 +++++++++ plugins/nfc/ios/.gitignore | 10 + plugins/nfc/ios/Package.swift | 33 ++ plugins/nfc/ios/README.md | 3 + plugins/nfc/ios/Sources/NfcPlugin.swift | 521 ++++++++++++++++++ .../ios/Tests/PluginTests/PluginTests.swift | 12 + plugins/nfc/package.json | 32 ++ plugins/nfc/rollup.config.js | 7 + plugins/nfc/src/api-iife.js | 1 + plugins/nfc/src/error.rs | 25 + plugins/nfc/src/lib.rs | 84 +++ plugins/nfc/src/mobile.rs | 11 + plugins/nfc/src/models.rs | 119 ++++ plugins/nfc/tsconfig.json | 4 + pnpm-lock.yaml | 404 ++------------ 53 files changed, 2586 insertions(+), 370 deletions(-) create mode 100644 .changes/nfc-initial-release.md create mode 100644 examples/api/src-tauri/gen/android/.idea/.gitignore create mode 100644 examples/api/src-tauri/gen/android/.idea/compiler.xml create mode 100644 examples/api/src-tauri/gen/android/.idea/gradle.xml create mode 100644 examples/api/src-tauri/gen/android/.idea/jarRepositories.xml create mode 100644 examples/api/src-tauri/gen/android/.idea/kotlinc.xml create mode 100644 examples/api/src-tauri/gen/android/.idea/misc.xml create mode 100644 examples/api/src-tauri/gen/android/.idea/vcs.xml create mode 100644 examples/api/src/views/Nfc.svelte create mode 100644 plugins/nfc/.gitignore create mode 100644 plugins/nfc/Cargo.toml create mode 100644 plugins/nfc/LICENSE.spdx create mode 100644 plugins/nfc/LICENSE_APACHE-2.0 create mode 100644 plugins/nfc/LICENSE_MIT create mode 100644 plugins/nfc/README.md create mode 100644 plugins/nfc/android/.gitignore create mode 100644 plugins/nfc/android/build.gradle.kts create mode 100644 plugins/nfc/android/proguard-rules.pro create mode 100644 plugins/nfc/android/settings.gradle create mode 100644 plugins/nfc/android/src/androidTest/java/ExampleInstrumentedTest.kt create mode 100644 plugins/nfc/android/src/main/AndroidManifest.xml create mode 100644 plugins/nfc/android/src/main/java/NfcPlugin.kt create mode 100644 plugins/nfc/android/src/main/res/xml/nfc_tech_filter.xml create mode 100644 plugins/nfc/android/src/test/java/ExampleUnitTest.kt create mode 100644 plugins/nfc/build.rs create mode 100644 plugins/nfc/contributors/crabnebula.svg create mode 100644 plugins/nfc/contributors/impierce.svg create mode 100644 plugins/nfc/guest-js/index.ts create mode 100644 plugins/nfc/ios/.gitignore create mode 100644 plugins/nfc/ios/Package.swift create mode 100644 plugins/nfc/ios/README.md create mode 100644 plugins/nfc/ios/Sources/NfcPlugin.swift create mode 100644 plugins/nfc/ios/Tests/PluginTests/PluginTests.swift create mode 100644 plugins/nfc/package.json create mode 100644 plugins/nfc/rollup.config.js create mode 100644 plugins/nfc/src/api-iife.js create mode 100644 plugins/nfc/src/error.rs create mode 100644 plugins/nfc/src/lib.rs create mode 100644 plugins/nfc/src/mobile.rs create mode 100644 plugins/nfc/src/models.rs create mode 100644 plugins/nfc/tsconfig.json diff --git a/.changes/config.json b/.changes/config.json index 399448f5..42de6fec 100644 --- a/.changes/config.json +++ b/.changes/config.json @@ -189,6 +189,15 @@ "manager": "javascript" }, + "nfc": { + "path": "./plugins/nfc", + "manager": "rust" + }, + "nfc-js": { + "path": "./plugins/nfc", + "manager": "javascript" + }, + "notification": { "path": "./plugins/notification", "manager": "rust" diff --git a/.changes/nfc-initial-release.md b/.changes/nfc-initial-release.md new file mode 100644 index 00000000..0a5ee02b --- /dev/null +++ b/.changes/nfc-initial-release.md @@ -0,0 +1,6 @@ +--- +"nfc": major +"nfc-js": major +--- + +Initial release. diff --git a/Cargo.lock b/Cargo.lock index 084d891f..eeb2dec2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -245,6 +245,7 @@ dependencies = [ "tauri-plugin-global-shortcut", "tauri-plugin-http", "tauri-plugin-log", + "tauri-plugin-nfc", "tauri-plugin-notification", "tauri-plugin-os", "tauri-plugin-process", @@ -6023,6 +6024,19 @@ dependencies = [ "time 0.3.30", ] +[[package]] +name = "tauri-plugin-nfc" +version = "1.0.0" +dependencies = [ + "log", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-build", + "thiserror", +] + [[package]] name = "tauri-plugin-notification" version = "2.0.0-alpha.6" diff --git a/examples/api/package.json b/examples/api/package.json index 3d076b42..e1135b54 100644 --- a/examples/api/package.json +++ b/examples/api/package.json @@ -17,6 +17,7 @@ "@tauri-apps/plugin-fs": "2.0.0-alpha.4", "@tauri-apps/plugin-global-shortcut": "2.0.0-alpha.4", "@tauri-apps/plugin-http": "2.0.0-alpha.4", + "@tauri-apps/plugin-nfc": "1.0.0", "@tauri-apps/plugin-notification": "2.0.0-alpha.4", "@tauri-apps/plugin-os": "2.0.0-alpha.5", "@tauri-apps/plugin-process": "2.0.0-alpha.4", diff --git a/examples/api/src-tauri/Cargo.toml b/examples/api/src-tauri/Cargo.toml index c7448e47..5ab61ed6 100644 --- a/examples/api/src-tauri/Cargo.toml +++ b/examples/api/src-tauri/Cargo.toml @@ -47,6 +47,7 @@ tauri-plugin-updater = { path = "../../../plugins/updater", version = "2.0.0-alp [target."cfg(any(target_os = \"android\", target_os = \"ios\"))".dependencies] tauri-plugin-barcode-scanner = { path = "../../../plugins/barcode-scanner/", version = "2.0.0-alpha.3" } +tauri-plugin-nfc = { path = "../../../plugins/nfc", version = "1.0.0" } [target."cfg(target_os = \"windows\")".dependencies] window-shadows = "0.2" diff --git a/examples/api/src-tauri/gen/android/.idea/.gitignore b/examples/api/src-tauri/gen/android/.idea/.gitignore new file mode 100644 index 00000000..26d33521 --- /dev/null +++ b/examples/api/src-tauri/gen/android/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/examples/api/src-tauri/gen/android/.idea/compiler.xml b/examples/api/src-tauri/gen/android/.idea/compiler.xml new file mode 100644 index 00000000..b589d56e --- /dev/null +++ b/examples/api/src-tauri/gen/android/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/examples/api/src-tauri/gen/android/.idea/gradle.xml b/examples/api/src-tauri/gen/android/.idea/gradle.xml new file mode 100644 index 00000000..b9c0e8d6 --- /dev/null +++ b/examples/api/src-tauri/gen/android/.idea/gradle.xml @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/examples/api/src-tauri/gen/android/.idea/jarRepositories.xml b/examples/api/src-tauri/gen/android/.idea/jarRepositories.xml new file mode 100644 index 00000000..d2ce72d1 --- /dev/null +++ b/examples/api/src-tauri/gen/android/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/api/src-tauri/gen/android/.idea/kotlinc.xml b/examples/api/src-tauri/gen/android/.idea/kotlinc.xml new file mode 100644 index 00000000..0fc31131 --- /dev/null +++ b/examples/api/src-tauri/gen/android/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/examples/api/src-tauri/gen/android/.idea/misc.xml b/examples/api/src-tauri/gen/android/.idea/misc.xml new file mode 100644 index 00000000..8978d23d --- /dev/null +++ b/examples/api/src-tauri/gen/android/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/examples/api/src-tauri/gen/android/.idea/vcs.xml b/examples/api/src-tauri/gen/android/.idea/vcs.xml new file mode 100644 index 00000000..bc599707 --- /dev/null +++ b/examples/api/src-tauri/gen/android/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/examples/api/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/examples/api/src-tauri/gen/android/app/src/main/AndroidManifest.xml index c9ef9e7f..4679a745 100644 --- a/examples/api/src-tauri/gen/android/app/src/main/AndroidManifest.xml +++ b/examples/api/src-tauri/gen/android/app/src/main/AndroidManifest.xml @@ -17,6 +17,26 @@ + + + + + + + + + + + + + + + + + + + version = "1.3"> + buildImplicitDependencies = "YES"> @@ -27,21 +26,16 @@ buildConfiguration = "debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "NO" - onlyGenerateCoverageForSpecifiedTargets = "NO"> + shouldUseLaunchSchemeArgsEnv = "NO"> - - - - + + - - - - + NFCReaderUsageDescription + NFC Test NSCameraUsageDescription Request camera access for barcode scanner CFBundleDevelopmentRegion diff --git a/examples/api/src-tauri/gen/apple/api_iOS/api_iOS.entitlements b/examples/api/src-tauri/gen/apple/api_iOS/api_iOS.entitlements index 0c67376e..9db395a2 100644 --- a/examples/api/src-tauri/gen/apple/api_iOS/api_iOS.entitlements +++ b/examples/api/src-tauri/gen/apple/api_iOS/api_iOS.entitlements @@ -1,5 +1,11 @@ - + + com.apple.developer.nfc.readersession.formats + + TAG + NDEF + + diff --git a/examples/api/src-tauri/src/lib.rs b/examples/api/src-tauri/src/lib.rs index ae23d314..8471daba 100644 --- a/examples/api/src-tauri/src/lib.rs +++ b/examples/api/src-tauri/src/lib.rs @@ -47,6 +47,7 @@ pub fn run() { #[cfg(mobile)] { app.handle().plugin(tauri_plugin_barcode_scanner::init())?; + app.handle().plugin(tauri_plugin_nfc::init())?; } let mut window_builder = WindowBuilder::new(app, "main", WindowUrl::default()); diff --git a/examples/api/src/App.svelte b/examples/api/src/App.svelte index 040d1939..f5ab054e 100644 --- a/examples/api/src/App.svelte +++ b/examples/api/src/App.svelte @@ -20,6 +20,7 @@ import { onMount } from "svelte"; import { ask } from "@tauri-apps/plugin-dialog"; + import Nfc from "./views/Nfc.svelte"; const appWindow = getCurrent(); @@ -107,6 +108,11 @@ component: Scanner, icon: "i-ph-scan", }, + isMobile && { + label: "NFC", + component: Nfc, + icon: "i-ph-nfc", + }, ]; let selected = views[0]; @@ -221,7 +227,7 @@ let isWindows; onMount(async () => { - isWindows = (await os.platform()) === "win32"; + isWindows = (await os.platform()) === "windows"; }); // mobile diff --git a/examples/api/src/views/Nfc.svelte b/examples/api/src/views/Nfc.svelte new file mode 100644 index 00000000..4d7ea2c3 --- /dev/null +++ b/examples/api/src/views/Nfc.svelte @@ -0,0 +1,126 @@ + + +
+
+
+ + +
+ + +
+ + {#if isAndroid} +
+

Filters

+
+ + + +
+
+ +
+
+ {/if} + +
+

Write Records

+
+ + +
+
+ + +
diff --git a/plugins/deep-link/examples/app/package.json b/plugins/deep-link/examples/app/package.json index b9866e22..5bded619 100644 --- a/plugins/deep-link/examples/app/package.json +++ b/plugins/deep-link/examples/app/package.json @@ -17,6 +17,6 @@ "@tauri-apps/cli": "2.0.0-alpha.18", "internal-ip": "^8.0.0", "typescript": "^5.2.2", - "vite": "^4.5.0" + "vite": "^5.0.6" } } diff --git a/plugins/nfc/.gitignore b/plugins/nfc/.gitignore new file mode 100644 index 00000000..1b0b469d --- /dev/null +++ b/plugins/nfc/.gitignore @@ -0,0 +1 @@ +/.tauri diff --git a/plugins/nfc/Cargo.toml b/plugins/nfc/Cargo.toml new file mode 100644 index 00000000..88808be7 --- /dev/null +++ b/plugins/nfc/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "tauri-plugin-nfc" +version = "1.0.0" +edition = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +links = "tauri-plugin-nfc" + +[package.metadata.docs.rs] +rustc-args = [ "--cfg", "docsrs" ] +rustdoc-args = [ "--cfg", "docsrs" ] + +[build-dependencies] +tauri-build = { workspace = true } + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +tauri = { workspace = true } +log = { workspace = true } +thiserror = { workspace = true } +serde_repr = "0.1" diff --git a/plugins/nfc/LICENSE.spdx b/plugins/nfc/LICENSE.spdx new file mode 100644 index 00000000..cdd0df5a --- /dev/null +++ b/plugins/nfc/LICENSE.spdx @@ -0,0 +1,20 @@ +SPDXVersion: SPDX-2.1 +DataLicense: CC0-1.0 +PackageName: tauri +DataFormat: SPDXRef-1 +PackageSupplier: Organization: The Tauri Programme in the Commons Conservancy +PackageHomePage: https://tauri.app +PackageLicenseDeclared: Apache-2.0 +PackageLicenseDeclared: MIT +PackageCopyrightText: 2019-2022, The Tauri Programme in the Commons Conservancy +PackageSummary: Tauri is a rust project that enables developers to make secure +and small desktop applications using a web frontend. + +PackageComment: The package includes the following libraries; see +Relationship information. + +Created: 2019-05-20T09:00:00Z +PackageDownloadLocation: git://github.com/tauri-apps/tauri +PackageDownloadLocation: git+https://github.com/tauri-apps/tauri.git +PackageDownloadLocation: git+ssh://github.com/tauri-apps/tauri.git +Creator: Person: Daniel Thompson-Yvetot \ No newline at end of file diff --git a/plugins/nfc/LICENSE_APACHE-2.0 b/plugins/nfc/LICENSE_APACHE-2.0 new file mode 100644 index 00000000..4947287f --- /dev/null +++ b/plugins/nfc/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/plugins/nfc/LICENSE_MIT b/plugins/nfc/LICENSE_MIT new file mode 100644 index 00000000..4d754725 --- /dev/null +++ b/plugins/nfc/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - Present Tauri Apps Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/plugins/nfc/README.md b/plugins/nfc/README.md new file mode 100644 index 00000000..ded44056 --- /dev/null +++ b/plugins/nfc/README.md @@ -0,0 +1,113 @@ +![NFC](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/nfc/banner.png) + + + +## Install + +_This plugin requires a Rust version of at least **1.65**_ + +There are three general methods of installation that we can recommend. + +1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) +2. Pull sources directly from Github using git tags / revision hashes (most secure) +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) + +Install the Core plugin by adding the following to your `Cargo.toml` file: + +`src-tauri/Cargo.toml` + +```toml +[dependencies] +tauri-plugin-nfc = "2.0.0-alpha" +# alternatively with Git: +tauri-plugin-nfc = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } +``` + +You can install the JavaScript Guest bindings using your preferred JavaScript package manager: + +> 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. + + + +```sh +pnpm add @tauri-apps/plugin-nfc +# or +npm add @tauri-apps/plugin-nfc +# or +yarn add @tauri-apps/plugin-nfc + +# alternatively with Git: +pnpm add https://github.com/tauri-apps/tauri-plugin-nfc#v2 +# or +npm add https://github.com/tauri-apps/tauri-plugin-nfc#v2 +# or +yarn add https://github.com/tauri-apps/tauri-plugin-nfc#v2 +``` + +## Usage + +First you need to register the core plugin with Tauri: + +`src-tauri/src/main.rs` + +```rust +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_nfc::init()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +Afterwards all the plugin's APIs are available through the JavaScript guest bindings: + +```javascript +import { scan, textRecord, write } from "@tauri-apps/plugin-nfc"; +await scan({ type: "tag", keepSessionAlive: true }); +await write([textRecord("Tauri is awesome!")]); +``` + +## Contributing + +PRs accepted. Please make sure to read the Contributing Guide before making a pull request. + +## Contributed By + + + + + + + + +
+ + CrabNebula + + + + Impierce + +
+ +## Partners + + + + + + + +
+ + CrabNebula + +
+ +For the complete list of sponsors please visit our [website](https://tauri.app#sponsors) and [Open Collective](https://opencollective.com/tauri). + +## License + +Code: (c) 2015 - Present - The Tauri Programme within The Commons Conservancy. + +MIT or MIT/Apache 2.0 where applicable. diff --git a/plugins/nfc/android/.gitignore b/plugins/nfc/android/.gitignore new file mode 100644 index 00000000..c0f21ec2 --- /dev/null +++ b/plugins/nfc/android/.gitignore @@ -0,0 +1,2 @@ +/build +/.tauri diff --git a/plugins/nfc/android/build.gradle.kts b/plugins/nfc/android/build.gradle.kts new file mode 100644 index 00000000..66eb414f --- /dev/null +++ b/plugins/nfc/android/build.gradle.kts @@ -0,0 +1,46 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "app.tauri.nfc" + compileSdk = 33 + + defaultConfig { + minSdk = 24 + targetSdk = 33 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + + implementation("androidx.core:core-ktx:1.9.0") + implementation("androidx.appcompat:appcompat:1.6.0") + implementation("com.google.android.material:material:1.7.0") + implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + implementation(project(":tauri-android")) +} diff --git a/plugins/nfc/android/proguard-rules.pro b/plugins/nfc/android/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/plugins/nfc/android/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/plugins/nfc/android/settings.gradle b/plugins/nfc/android/settings.gradle new file mode 100644 index 00000000..14a752e4 --- /dev/null +++ b/plugins/nfc/android/settings.gradle @@ -0,0 +1,2 @@ +include ':tauri-android' +project(':tauri-android').projectDir = new File('./.tauri/tauri-api') diff --git a/plugins/nfc/android/src/androidTest/java/ExampleInstrumentedTest.kt b/plugins/nfc/android/src/androidTest/java/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..f0f7b66e --- /dev/null +++ b/plugins/nfc/android/src/androidTest/java/ExampleInstrumentedTest.kt @@ -0,0 +1,28 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.nfc + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("app.tauri.nfc", appContext.packageName) + } +} diff --git a/plugins/nfc/android/src/main/AndroidManifest.xml b/plugins/nfc/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a2c208e2 --- /dev/null +++ b/plugins/nfc/android/src/main/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/plugins/nfc/android/src/main/java/NfcPlugin.kt b/plugins/nfc/android/src/main/java/NfcPlugin.kt new file mode 100644 index 00000000..5aa33732 --- /dev/null +++ b/plugins/nfc/android/src/main/java/NfcPlugin.kt @@ -0,0 +1,521 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.nfc + +import android.app.Activity +import android.app.PendingIntent +import android.content.Intent +import android.content.IntentFilter +import android.nfc.NdefMessage +import android.nfc.NdefRecord +import android.nfc.NfcAdapter +import android.nfc.Tag +import android.nfc.tech.Ndef +import android.nfc.tech.NdefFormatable +import android.os.Build +import android.os.Parcelable +import android.os.PatternMatcher +import android.webkit.WebView +import app.tauri.Logger +import app.tauri.annotation.Command +import app.tauri.annotation.InvokeArg +import app.tauri.annotation.TauriPlugin +import app.tauri.plugin.Invoke +import app.tauri.plugin.JSArray +import app.tauri.plugin.JSObject +import app.tauri.plugin.Plugin +import com.fasterxml.jackson.annotation.JsonValue +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import org.json.JSONArray +import java.io.IOException +import kotlin.concurrent.thread + +sealed class NfcAction { + object Read : NfcAction() + data class Write(val message: NdefMessage) : NfcAction() +} + +@InvokeArg +class UriFilter { + var scheme: String? = null + var host: String? = null + var pathPrefix: String? = null +} + +@InvokeArg +enum class TechKind(@JsonValue val value: String) { + IsoDep("IsoDep"), + MifareClassic("MifareClassic"), + MifareUltralight("MifareUltralight"), + Ndef("Ndef"), + NdefFormatable("NdefFormatable"), + NfcA("NfcA"), + NfcB("NfcB"), + NfcBarcode("NfcBarcode"), + NfcF("NfcF"), + NfcV("NfcV"); + + fun className(): String { + return when (this) { + IsoDep -> { + android.nfc.tech.IsoDep::class.java.name + } + MifareClassic -> { + android.nfc.tech.MifareClassic::class.java.name + } + MifareUltralight -> { + android.nfc.tech.MifareUltralight::class.java.name + } + Ndef -> { + android.nfc.tech.Ndef::class.java.name + } + NdefFormatable -> { + android.nfc.tech.NdefFormatable::class.java.name + } + NfcA -> { + android.nfc.tech.NfcA::class.java.name + } + NfcB -> { + android.nfc.tech.NfcB::class.java.name + } + NfcBarcode -> { + android.nfc.tech.NfcBarcode::class.java.name + } + NfcF -> { + android.nfc.tech.NfcF::class.java.name + } + NfcV -> { + android.nfc.tech.NfcV::class.java.name + } + } + } +} + +private fun addDataFilters(intentFilter: IntentFilter, uri: UriFilter?, mimeType: String?) { + uri?.let { it -> { + it.scheme?.let { + intentFilter.addDataScheme(it) + } + it.host?.let { + intentFilter.addDataAuthority(it, null) + } + it.pathPrefix?.let { + intentFilter.addDataPath(it, PatternMatcher.PATTERN_PREFIX) + } + }} + mimeType?.let { + intentFilter.addDataType(it) + } +} + +@InvokeArg +@JsonDeserialize(using = ScanKindDeserializer::class) +sealed class ScanKind { + @JsonDeserialize + class Tag: ScanKind() { + var mimeType: String? = null + var uri: UriFilter? = null + } + @JsonDeserialize + class Ndef: ScanKind() { + var mimeType: String? = null + var uri: UriFilter? = null + var techLists: Array>? = null + } + + fun filters(): Array? { + return when (this) { + is Tag -> { + val intentFilter = IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED) + addDataFilters(intentFilter, uri, mimeType) + arrayOf(intentFilter) + } + is Ndef -> { + val intentFilter = IntentFilter(if (techLists == null) NfcAdapter.ACTION_NDEF_DISCOVERED else NfcAdapter.ACTION_TECH_DISCOVERED) + addDataFilters(intentFilter, uri, mimeType) + arrayOf(intentFilter) + } + else -> null + } + } + + fun techLists(): Array>? { + return when (this) { + is Tag -> null + is Ndef -> { + techLists?.let { + val techs = mutableListOf>() + for (techList in it) { + val list = mutableListOf() + for (tech in techList) { + list.add(tech.className()) + } + techs.add(list.toTypedArray()) + } + techs.toTypedArray() + } ?: run { + null + } + } + else -> null + } + } +} + +internal class ScanKindDeserializer: JsonDeserializer() { + override fun deserialize( + jsonParser: JsonParser, + deserializationContext: DeserializationContext + ): ScanKind { + val node: JsonNode = jsonParser.codec.readTree(jsonParser) + node.get("tag")?.let { + return jsonParser.codec.treeToValue(it, ScanKind.Tag::class.java) + } ?: node.get("ndef")?.let { + return jsonParser.codec.treeToValue(it, ScanKind.Ndef::class.java) + } ?: run { + throw Error("unknown scan kind $node") + } + } +} + +@InvokeArg +class ScanOptions { + lateinit var kind: ScanKind + var keepSessionAlive: Boolean = false +} + +@InvokeArg +class NDEFRecordData { + var format: Short = 0 + var kind: ByteArray = ByteArray(0) + var id: ByteArray = ByteArray(0) + var payload: ByteArray = ByteArray(0) +} + +@InvokeArg +class WriteOptions { + var kind: ScanKind? = null + lateinit var records: Array +} + +class Session( + val action: NfcAction, + val invoke: Invoke, + val keepAlive: Boolean, + var tag: Tag? = null, + val filters: Array? = null, + val techLists: Array>? = null +) + +@TauriPlugin +class NfcPlugin(private val activity: Activity) : Plugin(activity) { + private lateinit var webView: WebView + + private var nfcAdapter: NfcAdapter? = null + private var session: Session? = null + + override fun load(webView: WebView) { + super.load(webView) + this.webView = webView + this.nfcAdapter = NfcAdapter.getDefaultAdapter(activity.applicationContext) + } + + override fun onNewIntent(intent: Intent) { + Logger.info("NFC", "onNewIntent") + super.onNewIntent(intent) + + val extraTag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(NfcAdapter.EXTRA_TAG, Tag::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableExtra(NfcAdapter.EXTRA_TAG) + } + + extraTag?.let { tag -> + session?.let { + if (it.keepAlive) { + it.tag = tag + } + } + + when (session?.action) { + is NfcAction.Read -> readTag(tag, intent) + is NfcAction.Write -> thread { + if (session?.action is NfcAction.Write) { + try { + writeTag(tag, (session?.action as NfcAction.Write).message) + session?.invoke?.resolve() + } catch (e: Exception) { + session?.invoke?.reject(e.toString()) + } finally { + if (this.session?.keepAlive != true) { + this.session = null + disableNFCInForeground() + } + } + } + } + + else -> {} + } + } + + } + + override fun onPause() { + disableNFCInForeground() + super.onPause() + Logger.info("NFC", "onPause") + } + + override fun onResume() { + super.onResume() + Logger.info("NFC", "onResume") + session?.let { + enableNFCInForeground(it.filters, it.techLists) + } + } + + private fun isAvailable(): Pair { + val available: Boolean + var errorReason: String? = null + + if (this.nfcAdapter === null) { + available = false + errorReason = "Device does not have NFC capabilities" + } else if (this.nfcAdapter?.isEnabled == false) { + available = false + errorReason = "NFC is disabled in device settings" + } else { + available = true + } + + return Pair(available, errorReason) + } + + @Command + fun isAvailable(invoke: Invoke) { + val ret = JSObject() + ret.put("available", isAvailable().first) + invoke.resolve(ret) + } + + @Command + fun scan(invoke: Invoke) { + val status = isAvailable() + if (!status.first) { + invoke.reject("NFC unavailable: " + status.second) + return + } + + val args = invoke.parseArgs(ScanOptions::class.java) + + val filters = args.kind.filters() + val techLists = args.kind.techLists() + enableNFCInForeground(filters, techLists) + + session = Session(NfcAction.Read, invoke, args.keepSessionAlive, null, filters, techLists) + } + + @Command + fun write(invoke: Invoke) { + val status = isAvailable() + if (!status.first) { + invoke.reject("NFC unavailable: " + status.second) + return + } + + val args = invoke.parseArgs(WriteOptions::class.java) + + val ndefRecords: MutableList = ArrayList() + for (record in args.records) { + ndefRecords.add(NdefRecord(record.format, record.kind, record.id, record.payload)) + } + + val message = NdefMessage(ndefRecords.toTypedArray()) + + session?.let { session -> + session.tag?.let { + try { + writeTag(it, message) + invoke.resolve() + } catch (e: Exception) { + invoke.reject(e.toString()) + } finally { + if (this.session?.keepAlive != true) { + this.session = null + disableNFCInForeground() + } + } + } ?: run { + invoke.reject("connected tag not found, please wait for it to be available and then call write()") + } + } ?: run { + args.kind?.let { kind -> { + val filters = kind.filters() + val techLists = kind.techLists() + enableNFCInForeground(filters, techLists) + session = Session(NfcAction.Write(message), invoke, true, null, filters, techLists) + Logger.warn("NFC", "Write Mode Enabled") + }} ?: run { + invoke.reject("Missing `kind` for write") + } + + } + } + + private fun readTag(tag: Tag, intent: Intent) { + try { + val rawMessages = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, Parcelable::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES) + } + + when (intent.action) { + NfcAdapter.ACTION_NDEF_DISCOVERED -> { + // For some reason this one never triggers. + Logger.info("NFC", "new NDEF intent") + readTagInner(tag, rawMessages) + } + NfcAdapter.ACTION_TECH_DISCOVERED -> { + // For some reason this always triggers instead of NDEF_DISCOVERED even though we set ndef filters right now + Logger.info("NFC", "new TECH intent") + // TODO: handle different techs. Don't assume ndef. + readTagInner(tag, rawMessages) + } + NfcAdapter.ACTION_TAG_DISCOVERED -> { + // This should never trigger when an app handles NDEF and TECH + // TODO: Don't assume ndef. + readTagInner(tag, rawMessages) + } + } + } catch (e: Exception) { + session?.invoke?.reject("failed to read tag", e) + } finally { + if (this.session?.keepAlive != true) { + this.session = null + } + // TODO this crashes? disableNFCInForeground() + } + } + + private fun readTagInner(tag: Tag?, rawMessages: Array?) { + val ndefMessage = rawMessages?.get(0) as NdefMessage? + + val records = ndefMessage?.records ?: arrayOf() + + val jsonRecords = Array(records.size) { i -> recordToJson(records[i]) } + + val ret = JSObject() + if (tag !== null) { + ret.put("id", fromU8Array(tag.id)) + // TODO There's also ndef.type which returns the ndef spec type which may be interesting to know too? + ret.put("kind", JSArray.from(tag.techList)) + } + ret.put("records", JSArray.from(jsonRecords)) + + session?.invoke?.resolve(ret) + } + + private fun writeTag(tag: Tag, message: NdefMessage) { + // This should return tags that are already in ndef format + val ndefTag = Ndef.get(tag) + if (ndefTag !== null) { + // We have to connect first to check maxSize. + try { + ndefTag.connect() + } catch (e: IOException) { + throw Exception("Couldn't connect to NFC tag", e) + } + + if (ndefTag.maxSize < message.toByteArray().size) { + throw Exception("The message is too large for the provided NFC tag") + } else if (!ndefTag.isWritable) { + throw Exception("NFC tag is read-only") + } else { + try { + ndefTag.writeNdefMessage(message) + } catch (e: Exception) { + throw Exception("Couldn't write message to NFC tag", e) + } + } + + try { + ndefTag.close() + } catch (e: IOException) { + Logger.error("failed to close tag", e) + } + + return + } + + // This should cover tags that are not yet in ndef format but can be converted + val ndefFormatableTag = NdefFormatable.get(tag) + if (ndefFormatableTag !== null) { + try { + ndefFormatableTag.connect() + ndefFormatableTag.format(message) + } catch (e: Exception) { + throw Exception("Couldn't format tag as Ndef", e) + } + + try { + ndefFormatableTag.close() + } catch (e: IOException) { + Logger.error("failed to close tag", e) + } + + return + } + + // if we get to this line, the tag was neither Ndef nor NdefFormatable compatible + throw Exception("Tag doesn't support Ndef format") + } + + // TODO: Use ReaderMode instead of ForegroundDispatch + private fun enableNFCInForeground(filters: Array?, techLists: Array>?) { + val flag = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE else PendingIntent.FLAG_UPDATE_CURRENT + val pendingIntent = PendingIntent.getActivity( + activity, 0, + Intent( + activity, + activity.javaClass + ).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), + flag + ) + + nfcAdapter?.enableForegroundDispatch(activity, pendingIntent, filters, techLists) + } + + private fun disableNFCInForeground() { + activity.runOnUiThread { + nfcAdapter?.disableForegroundDispatch(activity) + } + } +} + +private fun fromU8Array(byteArray: ByteArray): JSONArray { + val json = JSONArray() + for (byte in byteArray) { + json.put(byte) + } + return json +} + +private fun recordToJson(record: NdefRecord): JSObject { + val json = JSObject() + json.put("tnf", record.tnf) + json.put("kind", fromU8Array(record.type)) + json.put("id", fromU8Array(record.id)) + json.put("payload", fromU8Array(record.payload)) + return json +} \ No newline at end of file diff --git a/plugins/nfc/android/src/main/res/xml/nfc_tech_filter.xml b/plugins/nfc/android/src/main/res/xml/nfc_tech_filter.xml new file mode 100644 index 00000000..994905a6 --- /dev/null +++ b/plugins/nfc/android/src/main/res/xml/nfc_tech_filter.xml @@ -0,0 +1,13 @@ + + + android.nfc.tech.IsoDep + android.nfc.tech.NfcA + android.nfc.tech.NfcB + android.nfc.tech.NfcF + android.nfc.tech.NfcV + android.nfc.tech.Ndef + android.nfc.tech.NdefFormatable + android.nfc.tech.MifareClassic + android.nfc.tech.MifareUltralight + + diff --git a/plugins/nfc/android/src/test/java/ExampleUnitTest.kt b/plugins/nfc/android/src/test/java/ExampleUnitTest.kt new file mode 100644 index 00000000..2af426f8 --- /dev/null +++ b/plugins/nfc/android/src/test/java/ExampleUnitTest.kt @@ -0,0 +1,21 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package app.tauri.nfc + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/plugins/nfc/build.rs b/plugins/nfc/build.rs new file mode 100644 index 00000000..2f932ecc --- /dev/null +++ b/plugins/nfc/build.rs @@ -0,0 +1,43 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::process::exit; + +fn main() { + if let Err(error) = tauri_build::mobile::PluginBuilder::new() + .android_path("android") + .ios_path("ios") + .run() + { + println!("{error:#}"); + exit(1); + } + + // TODO: triple check if this can reference the plugin's xml as it expects rn + // TODO: This has to be configurable if we want to support handling nfc tags when the app is not open. + tauri_build::mobile::update_android_manifest( + "NFC PLUGIN", + "activity", + r#" + + + + + + + + + + + + + + +"# + .to_string(), + ) + .expect("failed to rewrite AndroidManifest.xml"); +} diff --git a/plugins/nfc/contributors/crabnebula.svg b/plugins/nfc/contributors/crabnebula.svg new file mode 100644 index 00000000..a9bb4609 --- /dev/null +++ b/plugins/nfc/contributors/crabnebula.svg @@ -0,0 +1,31 @@ + \ No newline at end of file diff --git a/plugins/nfc/contributors/impierce.svg b/plugins/nfc/contributors/impierce.svg new file mode 100644 index 00000000..9d2a510b --- /dev/null +++ b/plugins/nfc/contributors/impierce.svg @@ -0,0 +1,21 @@ + \ No newline at end of file diff --git a/plugins/nfc/guest-js/index.ts b/plugins/nfc/guest-js/index.ts new file mode 100644 index 00000000..e1861ae4 --- /dev/null +++ b/plugins/nfc/guest-js/index.ts @@ -0,0 +1,272 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { invoke } from "@tauri-apps/api/primitives"; + +export const RTD_TEXT = [0x54]; // "T" +export const RTD_URI = [0x55]; // "U" + +export interface UriFilter { + scheme?: string; + host?: string; + pathPrefix?: string; +} + +export enum TechKind { + IsoDep, + MifareClassic, + MifareUltralight, + Ndef, + NdefFormatable, + NfcA, + NfcB, + NfcBarcode, + NfcF, + NfcV, +} + +export type ScanKind = + | { + type: "tag"; + uri?: UriFilter; + mimeType?: string; + } + | { + type: "ndef"; + uri?: UriFilter; + mimeType?: string; + /** + * Each of the tech-lists is considered independently and the activity is considered a match if + * any single tech-list matches the tag that was discovered. + * This provides AND and OR semantics for filtering desired techs. + * + * See for more information. + * + * Examples + * + * ```ts + * import type { TechKind } from "@tauri-apps/plugin-nfc" + * + * const techLists = [ + * // capture anything using NfcF + * [TechKind.NfcF], + * // capture all MIFARE Classics with NDEF payloads + * [TechKind.NfcA, TechKind.MifareClassic, TechKind.Ndef] + * ] + * ``` + */ + techLists?: TechKind[][]; + }; + +export interface ScanOptions { + keepSessionAlive?: boolean; + /** Message displayed in the UI. iOS only. */ + message?: string; + /** Message displayed in the UI when the message has been read. iOS only. */ + successMessage?: string; +} + +export interface WriteOptions { + kind?: ScanKind; + /** Message displayed in the UI when reading the tag. iOS only. */ + message?: string; + /** Message displayed in the UI when the tag has been read. iOS only. */ + successfulReadMessage?: string; + /** Message displayed in the UI when the message has been written. iOS only. */ + successMessage?: string; +} + +export enum NFCTypeNameFormat { + Empty = 0, + NfcWellKnown = 1, + Media = 2, + AbsoluteURI = 3, + NfcExternal = 4, + Unknown = 5, + Unchanged = 6, +} + +export interface TagRecord { + tnf: NFCTypeNameFormat; + kind: number[]; + id: number[]; + payload: number[]; +} + +export interface Tag { + id: number[]; + kind: string[]; + records: TagRecord[]; +} + +export interface NFCRecord { + format: NFCTypeNameFormat; + kind: number[]; + id: number[]; + payload: number[]; +} + +export function record( + format: NFCTypeNameFormat, + kind: string | number[], + id: string | number[], + payload: string | number[], +): NFCRecord { + return { + format, + kind: + typeof kind === "string" + ? Array.from(new TextEncoder().encode(kind)) + : kind, + id: typeof id === "string" ? Array.from(new TextEncoder().encode(id)) : id, + payload: + typeof payload === "string" + ? Array.from(new TextEncoder().encode(payload)) + : payload, + }; +} + +export function textRecord( + text: string, + id?: string | number[], + language: string = "en", +): NFCRecord { + const payload = Array.from(new TextEncoder().encode(language + text)); + payload.unshift(language.length); + return record(NFCTypeNameFormat.NfcWellKnown, RTD_TEXT, id || [], payload); +} + +const protocols = [ + "", + "http://www.", + "https://www.", + "http://", + "https://", + "tel:", + "mailto:", + "ftp://anonymous:anonymous@", + "ftp://ftp.", + "ftps://", + "sftp://", + "smb://", + "nfs://", + "ftp://", + "dav://", + "news:", + "telnet://", + "imap:", + "rtsp://", + "urn:", + "pop:", + "sip:", + "sips:", + "tftp:", + "btspp://", + "btl2cap://", + "btgoep://", + "tcpobex://", + "irdaobex://", + "file://", + "urn:epc:id:", + "urn:epc:tag:", + "urn:epc:pat:", + "urn:epc:raw:", + "urn:epc:", + "urn:nfc:", +]; + +function encodeURI(uri: string): number[] { + let prefix = ""; + + protocols.slice(1).forEach(function (protocol) { + if ((!prefix || prefix === "urn:") && uri.indexOf(protocol) === 0) { + prefix = protocol; + } + }); + + if (!prefix) { + prefix = ""; + } + + const encoded = Array.from( + new TextEncoder().encode(uri.slice(prefix.length)), + ); + const protocolCode = protocols.indexOf(prefix); + // prepend protocol code + encoded.unshift(protocolCode); + + return encoded; +} + +export function uriRecord(uri: string, id?: string | number[]): NFCRecord { + return record( + NFCTypeNameFormat.NfcWellKnown, + RTD_URI, + id || [], + encodeURI(uri), + ); +} + +function mapScanKind(kind: ScanKind): Record { + const { type: scanKind, ...kindOptions } = kind; + return { [scanKind]: kindOptions }; +} + +/** + * Scans an NFC tag. + * + * ```javascript + * import { scan } from "@tauri-apps/plugin-nfc"; + * await scan({ type: "tag" }); + * ``` + * + * See for more information. + * + * @param kind + * @param options + * @returns + */ +export async function scan( + kind: ScanKind, + options?: ScanOptions, +): Promise { + return await invoke("plugin:nfc|scan", { + kind: mapScanKind(kind), + ...options, + }); +} + +/** + * Write to an NFC tag. + * + * ```javascript + * import { uriRecord, write } from "@tauri-apps/plugin-nfc"; + * await write([uriRecord("https://tauri.app")], { kind: { type: "ndef" } }); + * ``` + * + * If you did not previously call {@link scan} with {@link ScanOptions.keepSessionAlive} set to true, + * it will first scan the tag then write to it. + * + * @param records + * @param options + * @returns + */ +export async function write( + records: NFCRecord[], + options?: WriteOptions, +): Promise { + const { kind, ...opts } = options || {}; + if (kind) { + // @ts-expect-error map the property + opts.kind = mapScanKind(kind); + } + return await invoke("plugin:nfc|write", { + records, + ...opts, + }); +} + +export async function isAvailable(): Promise { + return await invoke("plugin:nfc|isAvailable"); +} diff --git a/plugins/nfc/ios/.gitignore b/plugins/nfc/ios/.gitignore new file mode 100644 index 00000000..5922fdaa --- /dev/null +++ b/plugins/nfc/ios/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +Package.resolved diff --git a/plugins/nfc/ios/Package.swift b/plugins/nfc/ios/Package.swift new file mode 100644 index 00000000..e8f1f19a --- /dev/null +++ b/plugins/nfc/ios/Package.swift @@ -0,0 +1,33 @@ +// swift-tools-version:5.3 +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import PackageDescription + +let package = Package( + name: "tauri-plugin-nfc", + platforms: [ + .iOS(.v13) + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "tauri-plugin-nfc", + type: .static, + targets: ["tauri-plugin-nfc"]) + ], + dependencies: [ + .package(name: "Tauri", path: "../.tauri/tauri-api") + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "tauri-plugin-nfc", + dependencies: [ + .byName(name: "Tauri") + ], + path: "Sources") + ] +) diff --git a/plugins/nfc/ios/README.md b/plugins/nfc/ios/README.md new file mode 100644 index 00000000..88a429b7 --- /dev/null +++ b/plugins/nfc/ios/README.md @@ -0,0 +1,3 @@ +# Tauri Plugin Nfc + +A description of this package. diff --git a/plugins/nfc/ios/Sources/NfcPlugin.swift b/plugins/nfc/ios/Sources/NfcPlugin.swift new file mode 100644 index 00000000..21bb2606 --- /dev/null +++ b/plugins/nfc/ios/Sources/NfcPlugin.swift @@ -0,0 +1,521 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// https://developer.apple.com/documentation/corenfc/building_an_nfc_tag-reader_app + +import CoreNFC +import SwiftRs +import Tauri +import UIKit +import WebKit + +enum ScanKind: Decodable { + case ndef, tag +} + +struct ScanOptions: Decodable { + let kind: ScanKind + let keepSessionAlive: Bool? + let message: String? + let successMessage: String? +} + +struct NDEFRecord: Decodable { + let format: UInt8? + let kind: [UInt8]? + let identifier: [UInt8]? + let payload: [UInt8]? +} + +struct WriteOptions: Decodable { + let kind: ScanKind? + let records: [NDEFRecord] + let message: String? + let successMessage: String? + let successfulReadMessage: String? +} + +enum TagProcessMode { + case write(message: NFCNDEFMessage) + case read +} + +class Session { + let nfcSession: NFCReaderSession? + let invoke: Invoke + var keepAlive: Bool + let tagProcessMode: TagProcessMode + var tagStatus: NFCNDEFStatus? + var tag: NFCNDEFTag? + let successfulReadMessage: String? + let successfulWriteAlertMessage: String? + + init( + nfcSession: NFCReaderSession?, + invoke: Invoke, + keepAlive: Bool, + tagProcessMode: TagProcessMode, + successfulReadMessage: String?, + successfulWriteAlertMessage: String? + ) { + self.nfcSession = nfcSession + self.invoke = invoke + self.keepAlive = keepAlive + self.tagProcessMode = tagProcessMode + self.successfulReadMessage = successfulReadMessage + self.successfulWriteAlertMessage = successfulWriteAlertMessage + } +} + +class NfcStatus { + let available: Bool + let errorReason: String? + + init(available: Bool, errorReason: String?) { + self.available = available + self.errorReason = errorReason + } +} + +class NfcPlugin: Plugin, NFCTagReaderSessionDelegate, NFCNDEFReaderSessionDelegate { + var session: Session? + var status: NfcStatus! + + public override func load(webview: WKWebView) { + var available = false + var errorReason: String? + + let entry = Bundle.main.infoDictionary?["NFCReaderUsageDescription"] as? String + + if entry == nil || entry?.count == 0 { + errorReason = "missing NFCReaderUsageDescription configuration on the Info.plist file" + } else if !NFCNDEFReaderSession.readingAvailable { + errorReason = + "NFC tag reading unavailable, make sure the Near-Field Communication capability on Xcode is enabled and the device supports NFC tag reading" + } else { + available = true + } + + if let error = errorReason { + Logger.error("\(error)") + } + + self.status = NfcStatus(available: available, errorReason: errorReason) + } + + func tagReaderSessionDidBecomeActive( + _ session: NFCTagReaderSession + ) { + Logger.info("tagReaderSessionDidBecomeActive") + } + + func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { + let tag = tags.first! + + session.connect( + to: tag, + completionHandler: { [self] (error) in + if let error = error { + self.closeSession(session, error: "cannot connect to tag: \(error)") + + } else { + let ndefTag: NFCNDEFTag + switch tag { + case let .feliCa(tag): + ndefTag = tag as NFCNDEFTag + break + case let .miFare(tag): + ndefTag = tag as NFCNDEFTag + break + case let .iso15693(tag): + ndefTag = tag as NFCNDEFTag + break + case let .iso7816(tag): + ndefTag = tag as NFCNDEFTag + break + default: + return + } + + self.processTag( + session: session, tag: ndefTag, metadata: tagMetadata(tag), + mode: self.session!.tagProcessMode) + } + } + ) + } + + func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) { + Logger.error("Tag reader session error \(error)") + self.session?.invoke.reject("session invalidated with error: \(error)") + } + + func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) { + let message = messages.first! + // TODO: do we really need this hook? + self.session?.invoke.resolve(["records": ndefMessageRecords(message)]) + } + + func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag]) { + let tag = tags.first! + + session.connect( + to: tag, + completionHandler: { [self] (error) in + if let error = error { + self.closeSession(session, error: "cannot connect to tag: \(error)") + + } else { + var metadata: JsonObject = [:] + if tag.isKind(of: NFCFeliCaTag.self) { + metadata["kind"] = ["FeliCa"] + metadata["id"] = nil + } else if let t = tag as? NFCMiFareTag { + metadata["kind"] = ["MiFare"] + metadata["id"] = byteArrayFromData(t.identifier) + } else if let t = tag as? NFCISO15693Tag { + metadata["kind"] = ["ISO15693"] + metadata["id"] = byteArrayFromData(t.identifier) + } else if let t = tag as? NFCISO7816Tag { + metadata["kind"] = ["ISO7816Compatible"] + metadata["id"] = byteArrayFromData(t.identifier) + } + + self.processTag( + session: session, tag: tag, metadata: metadata, + mode: self.session!.tagProcessMode) + } + } + ) + + } + + func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) { + if (error as NSError).code + == NFCReaderError.Code.readerSessionInvalidationErrorFirstNDEFTagRead.rawValue + { + // not an error because we're using invalidateAfterFirstRead: true + Logger.debug("readerSessionInvalidationErrorFirstNDEFTagRead") + } else { + Logger.error("NDEF reader session error \(error)") + self.session?.invoke.reject("session invalidated with error: \(error)") + } + } + + private func tagMetadata(_ tag: NFCTag) -> JsonObject { + var metadata: JsonObject = [:] + + switch tag { + case .feliCa: + metadata["kind"] = ["FeliCa"] + metadata["id"] = [] + break + case let .miFare(tag): + metadata["kind"] = ["MiFare"] + metadata["id"] = byteArrayFromData(tag.identifier) + break + case let .iso15693(tag): + metadata["kind"] = ["ISO15693"] + metadata["id"] = byteArrayFromData(tag.identifier) + break + case let .iso7816(tag): + metadata["kind"] = ["ISO7816Compatible"] + metadata["id"] = byteArrayFromData(tag.identifier) + break + default: + metadata["kind"] = ["Unknown"] + metadata["id"] = [] + break + } + + return metadata + } + + private func closeSession(_ session: NFCReaderSession) { + session.invalidate() + self.session = nil + } + + private func closeSession(_ session: NFCReaderSession, error: String) { + session.invalidate(errorMessage: error) + self.session = nil + } + + private func processTag( + session: NFCReaderSession, tag: T, metadata: JsonObject, mode: TagProcessMode + ) { + tag.queryNDEFStatus(completionHandler: { + [self] (status, capacity, error) in + if let error = error { + self.closeSession(session, error: "cannot connect to tag: \(error)") + } else { + switch mode { + case .write(let message): + self.writeNDEFTag( + session: session, status: status, tag: tag, message: message, + alertMessage: self.session?.successfulWriteAlertMessage) + break + case .read: + if self.session?.keepAlive == true { + self.session!.tagStatus = status + self.session!.tag = tag + } + self.readNDEFTag( + session: session, status: status, tag: tag, metadata: metadata, + alertMessage: self.session?.successfulReadMessage) + break + } + } + }) + } + + private func writeNDEFTag( + session: NFCReaderSession, status: NFCNDEFStatus, tag: T, message: NFCNDEFMessage, + alertMessage: String? + ) { + switch status { + case .notSupported: + self.closeSession(session, error: "Tag is not an NDEF-formatted tag") + break + case .readOnly: + self.closeSession(session, error: "Read only tag") + break + case .readWrite: + if let currentSession = self.session { + tag.writeNDEF( + message, + completionHandler: { (error) in + if let error = error { + self.closeSession(session, error: "cannot write to tag: \(error)") + } else { + if let message = alertMessage { + session.alertMessage = message + } + currentSession.invoke.resolve() + + self.closeSession(session) + + } + }) + } + break + default: + return + } + } + + private func readNDEFTag( + session: NFCReaderSession, status: NFCNDEFStatus, tag: T, metadata m: JsonObject, + alertMessage: String? + ) { + var metadata: JsonObject = [:] + metadata.merge(m) { (_, new) in new } + + switch status { + case .notSupported: + self.resolveInvoke(message: nil, metadata: metadata) + self.closeSession(session) + return + case .readOnly: + metadata["readOnly"] = true + break + case .readWrite: + metadata["readOnly"] = false + break + default: + break + } + + tag.readNDEF(completionHandler: { + [self] (message, error) in + if let error = error { + let code = (error as NSError).code + if code != 403 { + self.closeSession(session, error: "Failed to read: \(error)") + return + } + } + + if let message = alertMessage { + session.alertMessage = message + } + self.resolveInvoke(message: message, metadata: metadata) + + if self.session?.keepAlive != true { + self.closeSession(session) + } + }) + } + + private func resolveInvoke(message: NFCNDEFMessage?, metadata: JsonObject) { + var data: JsonObject = [:] + + data.merge(metadata) { (_, new) in new } + + if let message = message { + data["records"] = ndefMessageRecords(message) + } else { + data["records"] = [] + } + + self.session?.invoke.resolve(data) + } + + private func ndefMessageRecords(_ message: NFCNDEFMessage) -> [JsonObject] { + var records: [JsonObject] = [] + for record in message.records { + var recordJson: JsonObject = [:] + recordJson["tnf"] = record.typeNameFormat.rawValue + recordJson["kind"] = byteArrayFromData(record.type) + recordJson["id"] = byteArrayFromData(record.identifier) + recordJson["payload"] = byteArrayFromData(record.payload) + + records.append(recordJson) + } + + return records + } + + private func byteArrayFromData(_ data: Data) -> [UInt8] { + var arr: [UInt8] = [] + for b in data { + arr.append(b) + } + return arr + } + + private func dataFromByteArray(_ array: [UInt8]) -> Data { + var data = Data(capacity: array.count) + + data.append(contentsOf: array) + + return data + } + + @objc func isAvailable(_ invoke: Invoke) { + invoke.resolve([ + "available": self.status.available + ]) + } + + @objc public func write(_ invoke: Invoke) throws { + if !self.status.available { + invoke.reject("NFC reading unavailable: \(self.status.errorReason ?? "")") + return + } + + let args = try invoke.parseArgs(WriteOptions.self) + + var ndefPayloads = [NFCNDEFPayload]() + + for record in args.records { + ndefPayloads.append( + NFCNDEFPayload( + format: NFCTypeNameFormat(rawValue: record.format ?? 0) ?? .unknown, + type: dataFromByteArray(record.kind ?? []), + identifier: dataFromByteArray(record.identifier ?? []), + payload: dataFromByteArray(record.payload ?? []) + ) + ) + } + + if let session = self.session { + if let nfcSession = session.nfcSession, let tagStatus = session.tagStatus, + let tag = session.tag + { + session.keepAlive = false + self.writeNDEFTag( + session: nfcSession, status: tagStatus, tag: tag, + message: NFCNDEFMessage(records: ndefPayloads), + alertMessage: args.successMessage + ) + } else { + invoke.reject( + "connected tag not found, please wait for it to be available and then call write()") + } + } else { + self.startScanSession( + invoke: invoke, + kind: args.kind ?? .ndef, + keepAlive: true, + invalidateAfterFirstRead: false, + tagProcessMode: .write( + message: NFCNDEFMessage(records: ndefPayloads) + ), + alertMessage: args.message, + successfulReadMessage: args.successfulReadMessage, + successfulWriteAlertMessage: args.successMessage + ) + } + } + + @objc public func scan(_ invoke: Invoke) throws { + if !self.status.available { + invoke.reject("NFC reading unavailable: \(self.status.errorReason ?? "")") + return + } + + let args = try invoke.parseArgs(ScanOptions.self) + + self.startScanSession( + invoke: invoke, + kind: args.kind, + keepAlive: args.keepSessionAlive ?? false, + invalidateAfterFirstRead: true, + tagProcessMode: .read, + alertMessage: args.message, + successfulReadMessage: args.successMessage, + successfulWriteAlertMessage: nil + ) + } + + private func startScanSession( + invoke: Invoke, + kind: ScanKind, + keepAlive: Bool, + invalidateAfterFirstRead: Bool, + tagProcessMode: TagProcessMode, + alertMessage: String?, + successfulReadMessage: String?, + successfulWriteAlertMessage: String? + ) { + let nfcSession: NFCReaderSession? + + switch kind { + case .tag: + nfcSession = NFCTagReaderSession( + pollingOption: [.iso14443, .iso15693], + delegate: self, + queue: DispatchQueue.main + ) + break + case .ndef: + nfcSession = NFCNDEFReaderSession( + delegate: self, + queue: DispatchQueue.main, + invalidateAfterFirstRead: invalidateAfterFirstRead + ) + break + } + + if let message = alertMessage { + nfcSession?.alertMessage = message + } + nfcSession?.begin() + + self.session = Session( + nfcSession: nfcSession, + invoke: invoke, + keepAlive: keepAlive, + tagProcessMode: tagProcessMode, + successfulReadMessage: successfulReadMessage, + successfulWriteAlertMessage: successfulWriteAlertMessage + ) + } +} + +@_cdecl("init_plugin_nfc") +func initPlugin() -> Plugin { + return NfcPlugin() +} diff --git a/plugins/nfc/ios/Tests/PluginTests/PluginTests.swift b/plugins/nfc/ios/Tests/PluginTests/PluginTests.swift new file mode 100644 index 00000000..99992ce4 --- /dev/null +++ b/plugins/nfc/ios/Tests/PluginTests/PluginTests.swift @@ -0,0 +1,12 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import XCTest +@testable import ExamplePlugin + +final class ExamplePluginTests: XCTestCase { + func testExample() throws { + let plugin = ExamplePlugin() + } +} diff --git a/plugins/nfc/package.json b/plugins/nfc/package.json new file mode 100644 index 00000000..4131ed01 --- /dev/null +++ b/plugins/nfc/package.json @@ -0,0 +1,32 @@ +{ + "name": "@tauri-apps/plugin-nfc", + "version": "1.0.0", + "license": "MIT or APACHE-2.0", + "authors": [ + "Tauri Programme within The Commons Conservancy" + ], + "type": "module", + "types": "./dist-js/index.d.ts", + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "exports": { + "types": "./dist-js/index.d.ts", + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs" + }, + "scripts": { + "build": "rollup -c" + }, + "files": [ + "dist-js", + "!dist-js/**/*.map", + "README.md", + "LICENSE" + ], + "devDependencies": { + "tslib": "2.6.0" + }, + "dependencies": { + "@tauri-apps/api": "2.0.0-alpha.12" + } +} diff --git a/plugins/nfc/rollup.config.js b/plugins/nfc/rollup.config.js new file mode 100644 index 00000000..977dfac8 --- /dev/null +++ b/plugins/nfc/rollup.config.js @@ -0,0 +1,7 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +import { createConfig } from "../../shared/rollup.config.js"; + +export default createConfig(); diff --git a/plugins/nfc/src/api-iife.js b/plugins/nfc/src/api-iife.js new file mode 100644 index 00000000..80156954 --- /dev/null +++ b/plugins/nfc/src/api-iife.js @@ -0,0 +1 @@ +if("__TAURI__"in window){var __TAURI_PLUGIN_NFC__=function(n){"use strict";async function e(n,e={},t){return window.__TAURI_INTERNALS__.invoke(n,e,t)}"function"==typeof SuppressedError&&SuppressedError;const t=[84],r=[85];var o,c;function i(n,e,t,r){return{format:n,kind:"string"==typeof e?Array.from((new TextEncoder).encode(e)):e,id:"string"==typeof t?Array.from((new TextEncoder).encode(t)):t,payload:"string"==typeof r?Array.from((new TextEncoder).encode(r)):r}}n.TechKind=void 0,(o=n.TechKind||(n.TechKind={}))[o.IsoDep=0]="IsoDep",o[o.MifareClassic=1]="MifareClassic",o[o.MifareUltralight=2]="MifareUltralight",o[o.Ndef=3]="Ndef",o[o.NdefFormatable=4]="NdefFormatable",o[o.NfcA=5]="NfcA",o[o.NfcB=6]="NfcB",o[o.NfcBarcode=7]="NfcBarcode",o[o.NfcF=8]="NfcF",o[o.NfcV=9]="NfcV",n.NFCTypeNameFormat=void 0,(c=n.NFCTypeNameFormat||(n.NFCTypeNameFormat={}))[c.Empty=0]="Empty",c[c.NfcWellKnown=1]="NfcWellKnown",c[c.Media=2]="Media",c[c.AbsoluteURI=3]="AbsoluteURI",c[c.NfcExternal=4]="NfcExternal",c[c.Unknown=5]="Unknown",c[c.Unchanged=6]="Unchanged";const a=["","http://www.","https://www.","http://","https://","tel:","mailto:","ftp://anonymous:anonymous@","ftp://ftp.","ftps://","sftp://","smb://","nfs://","ftp://","dav://","news:","telnet://","imap:","rtsp://","urn:","pop:","sip:","sips:","tftp:","btspp://","btl2cap://","btgoep://","tcpobex://","irdaobex://","file://","urn:epc:id:","urn:epc:tag:","urn:epc:pat:","urn:epc:raw:","urn:epc:","urn:nfc:"];function f(n){const{type:e,...t}=n;return{[e]:t}}return n.RTD_TEXT=t,n.RTD_URI=r,n.isAvailable=async function(){return await e("plugin:nfc|isAvailable")},n.record=i,n.scan=async function(n,t){return await e("plugin:nfc|scan",{kind:f(n),...t})},n.textRecord=function(e,r,o="en"){const c=Array.from((new TextEncoder).encode(o+e));return c.unshift(o.length),i(n.NFCTypeNameFormat.NfcWellKnown,t,r||[],c)},n.uriRecord=function(e,t){return i(n.NFCTypeNameFormat.NfcWellKnown,r,t||[],function(n){let e="";a.slice(1).forEach((function(t){e&&"urn:"!==e||0!==n.indexOf(t)||(e=t)})),e||(e="");const t=Array.from((new TextEncoder).encode(n.slice(e.length))),r=a.indexOf(e);return t.unshift(r),t}(e))},n.write=async function(n,t){const{kind:r,...o}=t||{};return r&&(o.kind=f(r)),await e("plugin:nfc|write",{records:n,...o})},n}({});Object.defineProperty(window.__TAURI__,"nfc",{value:__TAURI_PLUGIN_NFC__})} diff --git a/plugins/nfc/src/error.rs b/plugins/nfc/src/error.rs new file mode 100644 index 00000000..339e763b --- /dev/null +++ b/plugins/nfc/src/error.rs @@ -0,0 +1,25 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{ser::Serializer, Serialize}; + +pub type Result = std::result::Result; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), + #[cfg(mobile)] + #[error(transparent)] + PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} diff --git a/plugins/nfc/src/lib.rs b/plugins/nfc/src/lib.rs new file mode 100644 index 00000000..28d5160d --- /dev/null +++ b/plugins/nfc/src/lib.rs @@ -0,0 +1,84 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#![cfg(mobile)] + +use serde::{Deserialize, Serialize}; +use tauri::{ + plugin::{Builder, PluginHandle, TauriPlugin}, + Manager, Runtime, +}; + +pub use models::*; + +mod error; +mod models; + +pub use error::{Error, Result}; + +#[cfg(target_os = "android")] +const PLUGIN_IDENTIFIER: &str = "app.tauri.nfc"; + +#[cfg(target_os = "ios")] +tauri::ios_plugin_binding!(init_plugin_nfc); + +/// Access to the nfc APIs. +pub struct Nfc(PluginHandle); + +#[derive(Deserialize)] +struct IsAvailableResponse { + available: bool, +} + +#[derive(Serialize)] +struct WriteRequest { + records: Vec, +} + +impl Nfc { + pub fn is_available(&self) -> crate::Result { + self.0 + .run_mobile_plugin::("isAvailable", ()) + .map(|r| r.available) + .map_err(Into::into) + } + + pub fn scan(&self, payload: ScanRequest) -> crate::Result { + self.0 + .run_mobile_plugin("scan", payload) + .map_err(Into::into) + } + + pub fn write(&self, records: Vec) -> crate::Result<()> { + self.0 + .run_mobile_plugin("write", WriteRequest { records }) + .map_err(Into::into) + } +} + +/// Extensions to [`tauri::App`], [`tauri::AppHandle`] and [`tauri::Window`] to access the nfc APIs. +pub trait NfcExt { + fn nfc(&self) -> &Nfc; +} + +impl> crate::NfcExt for T { + fn nfc(&self) -> &Nfc { + self.state::>().inner() + } +} + +/// Initializes the plugin. +pub fn init() -> TauriPlugin { + Builder::new("nfc") + .js_init_script(include_str!("api-iife.js").to_string()) + .setup(|app, api| { + #[cfg(target_os = "android")] + let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "NfcPlugin")?; + #[cfg(target_os = "ios")] + let handle = api.register_ios_plugin(init_plugin_nfc)?; + app.manage(Nfc(handle)); + Ok(()) + }) + .build() +} diff --git a/plugins/nfc/src/mobile.rs b/plugins/nfc/src/mobile.rs new file mode 100644 index 00000000..cf34d45e --- /dev/null +++ b/plugins/nfc/src/mobile.rs @@ -0,0 +1,11 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::de::DeserializeOwned; +use tauri::{ + plugin::{PluginApi, PluginHandle}, + AppHandle, Runtime, +}; + +use crate::models::*; diff --git a/plugins/nfc/src/models.rs b/plugins/nfc/src/models.rs new file mode 100644 index 00000000..eb05cf7a --- /dev/null +++ b/plugins/nfc/src/models.rs @@ -0,0 +1,119 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{Deserialize, Serialize, Serializer}; +use std::fmt::Display; + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ScanRequest { + pub kind: ScanKind, + pub keep_session_alive: bool, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NfcRecord { + pub format: NFCTypeNameFormat, + pub kind: Vec, + pub id: Vec, + pub payload: Vec, +} + +#[derive(serde_repr::Deserialize_repr, serde_repr::Serialize_repr)] +#[repr(u8)] +pub enum NFCTypeNameFormat { + Empty = 0, + NfcWellKnown = 1, + Media = 2, + AbsoluteURI = 3, + NfcExternal = 4, + Unknown = 5, + Unchanged = 6, +} + +#[derive(Deserialize)] +pub struct NfcTagRecord { + pub tnf: NFCTypeNameFormat, + pub kind: Vec, + pub id: Vec, + pub payload: Vec, +} + +#[derive(Deserialize)] +pub struct NfcTag { + pub id: String, + pub kind: String, + pub records: Vec, +} + +#[derive(Deserialize)] +pub struct ScanResponse { + pub tag: NfcTag, +} + +#[derive(Debug, Default, Serialize)] +pub struct UriFilter { + scheme: Option, + host: Option, + path_prefix: Option, +} + +#[derive(Debug)] +pub enum TechKind { + IsoDep, + MifareClassic, + MifareUltralight, + Ndef, + NdefFormatable, + NfcA, + NfcB, + NfcBarcode, + NfcF, + NfcV, +} + +impl Display for TechKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::IsoDep => "IsoDep", + Self::MifareClassic => "MifareClassic", + Self::MifareUltralight => "MifareUltralight", + Self::Ndef => "Ndef", + Self::NdefFormatable => "NdefFormatable", + Self::NfcA => "NfcA", + Self::NfcB => "NfcB", + Self::NfcBarcode => "NfcBarcode", + Self::NfcF => "NfcF", + Self::NfcV => "NfcV", + } + ) + } +} + +impl Serialize for TechKind { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum ScanKind { + Ndef { + mime_type: Option, + uri: Option, + tech_list: Option>>, + }, + Tag { + mime_type: Option, + uri: Option, + }, +} diff --git a/plugins/nfc/tsconfig.json b/plugins/nfc/tsconfig.json new file mode 100644 index 00000000..5098169a --- /dev/null +++ b/plugins/nfc/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["guest-js/*.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a19c1686..f67cd2c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,6 +87,9 @@ importers: '@tauri-apps/plugin-http': specifier: 2.0.0-alpha.4 version: link:../../plugins/http + '@tauri-apps/plugin-nfc': + specifier: 1.0.0 + version: link:../../plugins/nfc '@tauri-apps/plugin-notification': specifier: 2.0.0-alpha.4 version: link:../../plugins/notification @@ -129,7 +132,7 @@ importers: version: 4.2.8 unocss: specifier: ^0.58.0 - version: 0.58.0(postcss@8.4.32)(vite@5.0.6) + version: 0.58.0(postcss@8.4.32)(rollup@4.6.1)(vite@5.0.6) vite: specifier: ^5.0.6 version: 5.0.6 @@ -189,8 +192,8 @@ importers: specifier: ^5.2.2 version: 5.3.2 vite: - specifier: ^4.5.0 - version: 4.5.0 + specifier: ^5.0.6 + version: 5.0.6 plugins/dialog: dependencies: @@ -222,6 +225,16 @@ importers: specifier: 2.0.0-alpha.12 version: 2.0.0-alpha.12 + plugins/nfc: + dependencies: + '@tauri-apps/api': + specifier: 2.0.0-alpha.12 + version: 2.0.0-alpha.12 + devDependencies: + tslib: + specifier: 2.6.0 + version: 2.6.0 + plugins/notification: dependencies: '@tauri-apps/api': @@ -381,7 +394,7 @@ packages: '@babel/traverse': 7.23.5 '@babel/types': 7.23.5 convert-source-map: 2.0.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 7.5.4 @@ -651,7 +664,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.23.5 '@babel/types': 7.23.5 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -785,7 +798,7 @@ packages: peerDependencies: mocha: ^10.0.0 dependencies: - effection: 2.0.8 + effection: 2.0.8(mocha@10.2.0) mocha: 10.2.0 dev: true @@ -814,15 +827,6 @@ packages: '@effection/core': 2.2.3 dev: true - /@esbuild/android-arm64@0.18.14: - resolution: {integrity: sha512-rZ2v+Luba5/3D6l8kofWgTnqE+qsC/L5MleKIKFyllHTKHrNBMqeRCnZI1BtRx8B24xMYxeU32iIddRQqMsOsg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - /@esbuild/android-arm64@0.19.8: resolution: {integrity: sha512-B8JbS61bEunhfx8kasogFENgQfr/dIp+ggYXwTqdbMAgGDhRa3AaPpQMuQU0rNxDLECj6FhDzk1cF9WHMVwrtA==} engines: {node: '>=12'} @@ -832,15 +836,6 @@ packages: dev: true optional: true - /@esbuild/android-arm@0.18.14: - resolution: {integrity: sha512-blODaaL+lngG5bdK/t4qZcQvq2BBqrABmYwqPPcS5VRxrCSGHb9R/rA3fqxh7R18I7WU4KKv+NYkt22FDfalcg==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true - optional: true - /@esbuild/android-arm@0.19.8: resolution: {integrity: sha512-31E2lxlGM1KEfivQl8Yf5aYU/mflz9g06H6S15ITUFQueMFtFjESRMoDSkvMo8thYvLBax+VKTPlpnx+sPicOA==} engines: {node: '>=12'} @@ -850,15 +845,6 @@ packages: dev: true optional: true - /@esbuild/android-x64@0.18.14: - resolution: {integrity: sha512-qSwh8y38QKl+1Iqg+YhvCVYlSk3dVLk9N88VO71U4FUjtiSFylMWK3Ugr8GC6eTkkP4Tc83dVppt2n8vIdlSGg==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: true - optional: true - /@esbuild/android-x64@0.19.8: resolution: {integrity: sha512-rdqqYfRIn4jWOp+lzQttYMa2Xar3OK9Yt2fhOhzFXqg0rVWEfSclJvZq5fZslnz6ypHvVf3CT7qyf0A5pM682A==} engines: {node: '>=12'} @@ -868,15 +854,6 @@ packages: dev: true optional: true - /@esbuild/darwin-arm64@0.18.14: - resolution: {integrity: sha512-9Hl2D2PBeDYZiNbnRKRWuxwHa9v5ssWBBjisXFkVcSP5cZqzZRFBUWEQuqBHO4+PKx4q4wgHoWtfQ1S7rUqJ2Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - /@esbuild/darwin-arm64@0.19.8: resolution: {integrity: sha512-RQw9DemMbIq35Bprbboyf8SmOr4UXsRVxJ97LgB55VKKeJOOdvsIPy0nFyF2l8U+h4PtBx/1kRf0BelOYCiQcw==} engines: {node: '>=12'} @@ -886,15 +863,6 @@ packages: dev: true optional: true - /@esbuild/darwin-x64@0.18.14: - resolution: {integrity: sha512-ZnI3Dg4ElQ6tlv82qLc/UNHtFsgZSKZ7KjsUNAo1BF1SoYDjkGKHJyCrYyWjFecmXpvvG/KJ9A/oe0H12odPLQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - /@esbuild/darwin-x64@0.19.8: resolution: {integrity: sha512-3sur80OT9YdeZwIVgERAysAbwncom7b4bCI2XKLjMfPymTud7e/oY4y+ci1XVp5TfQp/bppn7xLw1n/oSQY3/Q==} engines: {node: '>=12'} @@ -904,15 +872,6 @@ packages: dev: true optional: true - /@esbuild/freebsd-arm64@0.18.14: - resolution: {integrity: sha512-h3OqR80Da4oQCIa37zl8tU5MwHQ7qgPV0oVScPfKJK21fSRZEhLE4IIVpmcOxfAVmqjU6NDxcxhYaM8aDIGRLw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/freebsd-arm64@0.19.8: resolution: {integrity: sha512-WAnPJSDattvS/XtPCTj1tPoTxERjcTpH6HsMr6ujTT+X6rylVe8ggxk8pVxzf5U1wh5sPODpawNicF5ta/9Tmw==} engines: {node: '>=12'} @@ -922,15 +881,6 @@ packages: dev: true optional: true - /@esbuild/freebsd-x64@0.18.14: - resolution: {integrity: sha512-ha4BX+S6CZG4BoH9tOZTrFIYC1DH13UTCRHzFc3GWX74nz3h/N6MPF3tuR3XlsNjMFUazGgm35MPW5tHkn2lzQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/freebsd-x64@0.19.8: resolution: {integrity: sha512-ICvZyOplIjmmhjd6mxi+zxSdpPTKFfyPPQMQTK/w+8eNK6WV01AjIztJALDtwNNfFhfZLux0tZLC+U9nSyA5Zg==} engines: {node: '>=12'} @@ -940,15 +890,6 @@ packages: dev: true optional: true - /@esbuild/linux-arm64@0.18.14: - resolution: {integrity: sha512-IXORRe22In7U65NZCzjwAUc03nn8SDIzWCnfzJ6t/8AvGx5zBkcLfknI+0P+hhuftufJBmIXxdSTbzWc8X/V4w==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-arm64@0.19.8: resolution: {integrity: sha512-z1zMZivxDLHWnyGOctT9JP70h0beY54xDDDJt4VpTX+iwA77IFsE1vCXWmprajJGa+ZYSqkSbRQ4eyLCpCmiCQ==} engines: {node: '>=12'} @@ -958,15 +899,6 @@ packages: dev: true optional: true - /@esbuild/linux-arm@0.18.14: - resolution: {integrity: sha512-5+7vehI1iqru5WRtJyU2XvTOvTGURw3OZxe3YTdE9muNNIdmKAVmSHpB3Vw2LazJk2ifEdIMt/wTWnVe5V98Kg==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-arm@0.19.8: resolution: {integrity: sha512-H4vmI5PYqSvosPaTJuEppU9oz1dq2A7Mr2vyg5TF9Ga+3+MGgBdGzcyBP7qK9MrwFQZlvNyJrvz6GuCaj3OukQ==} engines: {node: '>=12'} @@ -976,15 +908,6 @@ packages: dev: true optional: true - /@esbuild/linux-ia32@0.18.14: - resolution: {integrity: sha512-BfHlMa0nibwpjG+VXbOoqJDmFde4UK2gnW351SQ2Zd4t1N3zNdmUEqRkw/srC1Sa1DRBE88Dbwg4JgWCbNz/FQ==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-ia32@0.19.8: resolution: {integrity: sha512-1a8suQiFJmZz1khm/rDglOc8lavtzEMRo0v6WhPgxkrjcU0LkHj+TwBrALwoz/OtMExvsqbbMI0ChyelKabSvQ==} engines: {node: '>=12'} @@ -994,15 +917,6 @@ packages: dev: true optional: true - /@esbuild/linux-loong64@0.18.14: - resolution: {integrity: sha512-j2/Ex++DRUWIAaUDprXd3JevzGtZ4/d7VKz+AYDoHZ3HjJzCyYBub9CU1wwIXN+viOP0b4VR3RhGClsvyt/xSw==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-loong64@0.19.8: resolution: {integrity: sha512-fHZWS2JJxnXt1uYJsDv9+b60WCc2RlvVAy1F76qOLtXRO+H4mjt3Tr6MJ5l7Q78X8KgCFudnTuiQRBhULUyBKQ==} engines: {node: '>=12'} @@ -1012,15 +926,6 @@ packages: dev: true optional: true - /@esbuild/linux-mips64el@0.18.14: - resolution: {integrity: sha512-qn2+nc+ZCrJmiicoAnJXJJkZWt8Nwswgu1crY7N+PBR8ChBHh89XRxj38UU6Dkthl2yCVO9jWuafZ24muzDC/A==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-mips64el@0.19.8: resolution: {integrity: sha512-Wy/z0EL5qZYLX66dVnEg9riiwls5IYnziwuju2oUiuxVc+/edvqXa04qNtbrs0Ukatg5HEzqT94Zs7J207dN5Q==} engines: {node: '>=12'} @@ -1030,15 +935,6 @@ packages: dev: true optional: true - /@esbuild/linux-ppc64@0.18.14: - resolution: {integrity: sha512-aGzXzd+djqeEC5IRkDKt3kWzvXoXC6K6GyYKxd+wsFJ2VQYnOWE954qV2tvy5/aaNrmgPTb52cSCHFE+Z7Z0yg==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-ppc64@0.19.8: resolution: {integrity: sha512-ETaW6245wK23YIEufhMQ3HSeHO7NgsLx8gygBVldRHKhOlD1oNeNy/P67mIh1zPn2Hr2HLieQrt6tWrVwuqrxg==} engines: {node: '>=12'} @@ -1048,15 +944,6 @@ packages: dev: true optional: true - /@esbuild/linux-riscv64@0.18.14: - resolution: {integrity: sha512-8C6vWbfr0ygbAiMFLS6OPz0BHvApkT2gCboOGV76YrYw+sD/MQJzyITNsjZWDXJwPu9tjrFQOVG7zijRzBCnLw==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-riscv64@0.19.8: resolution: {integrity: sha512-T2DRQk55SgoleTP+DtPlMrxi/5r9AeFgkhkZ/B0ap99zmxtxdOixOMI570VjdRCs9pE4Wdkz7JYrsPvsl7eESg==} engines: {node: '>=12'} @@ -1066,15 +953,6 @@ packages: dev: true optional: true - /@esbuild/linux-s390x@0.18.14: - resolution: {integrity: sha512-G/Lf9iu8sRMM60OVGOh94ZW2nIStksEcITkXdkD09/T6QFD/o+g0+9WVyR/jajIb3A0LvBJ670tBnGe1GgXMgw==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-s390x@0.19.8: resolution: {integrity: sha512-NPxbdmmo3Bk7mbNeHmcCd7R7fptJaczPYBaELk6NcXxy7HLNyWwCyDJ/Xx+/YcNH7Im5dHdx9gZ5xIwyliQCbg==} engines: {node: '>=12'} @@ -1084,15 +962,6 @@ packages: dev: true optional: true - /@esbuild/linux-x64@0.18.14: - resolution: {integrity: sha512-TBgStYBQaa3EGhgqIDM+ECnkreb0wkcKqL7H6m+XPcGUoU4dO7dqewfbm0mWEQYH3kzFHrzjOFNpSAVzDZRSJw==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-x64@0.19.8: resolution: {integrity: sha512-lytMAVOM3b1gPypL2TRmZ5rnXl7+6IIk8uB3eLsV1JwcizuolblXRrc5ShPrO9ls/b+RTp+E6gbsuLWHWi2zGg==} engines: {node: '>=12'} @@ -1102,15 +971,6 @@ packages: dev: true optional: true - /@esbuild/netbsd-x64@0.18.14: - resolution: {integrity: sha512-stvCcjyCQR2lMTroqNhAbvROqRjxPEq0oQ380YdXxA81TaRJEucH/PzJ/qsEtsHgXlWFW6Ryr/X15vxQiyRXVg==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/netbsd-x64@0.19.8: resolution: {integrity: sha512-hvWVo2VsXz/8NVt1UhLzxwAfo5sioj92uo0bCfLibB0xlOmimU/DeAEsQILlBQvkhrGjamP0/el5HU76HAitGw==} engines: {node: '>=12'} @@ -1120,15 +980,6 @@ packages: dev: true optional: true - /@esbuild/openbsd-x64@0.18.14: - resolution: {integrity: sha512-apAOJF14CIsN5ht1PA57PboEMsNV70j3FUdxLmA2liZ20gEQnfTG5QU0FhENo5nwbTqCB2O3WDsXAihfODjHYw==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/openbsd-x64@0.19.8: resolution: {integrity: sha512-/7Y7u77rdvmGTxR83PgaSvSBJCC2L3Kb1M/+dmSIvRvQPXXCuC97QAwMugBNG0yGcbEGfFBH7ojPzAOxfGNkwQ==} engines: {node: '>=12'} @@ -1138,15 +989,6 @@ packages: dev: true optional: true - /@esbuild/sunos-x64@0.18.14: - resolution: {integrity: sha512-fYRaaS8mDgZcGybPn2MQbn1ZNZx+UXFSUoS5Hd2oEnlsyUcr/l3c6RnXf1bLDRKKdLRSabTmyCy7VLQ7VhGdOQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: true - optional: true - /@esbuild/sunos-x64@0.19.8: resolution: {integrity: sha512-9Lc4s7Oi98GqFA4HzA/W2JHIYfnXbUYgekUP/Sm4BG9sfLjyv6GKKHKKVs83SMicBF2JwAX6A1PuOLMqpD001w==} engines: {node: '>=12'} @@ -1156,15 +998,6 @@ packages: dev: true optional: true - /@esbuild/win32-arm64@0.18.14: - resolution: {integrity: sha512-1c44RcxKEJPrVj62XdmYhxXaU/V7auELCmnD+Ri+UCt+AGxTvzxl9uauQhrFso8gj6ZV1DaORV0sT9XSHOAk8Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - /@esbuild/win32-arm64@0.19.8: resolution: {integrity: sha512-rq6WzBGjSzihI9deW3fC2Gqiak68+b7qo5/3kmB6Gvbh/NYPA0sJhrnp7wgV4bNwjqM+R2AApXGxMO7ZoGhIJg==} engines: {node: '>=12'} @@ -1174,15 +1007,6 @@ packages: dev: true optional: true - /@esbuild/win32-ia32@0.18.14: - resolution: {integrity: sha512-EXAFttrdAxZkFQmpvcAQ2bywlWUsONp/9c2lcfvPUhu8vXBBenCXpoq9YkUvVP639ld3YGiYx0YUQ6/VQz3Maw==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - /@esbuild/win32-ia32@0.19.8: resolution: {integrity: sha512-AIAbverbg5jMvJznYiGhrd3sumfwWs8572mIJL5NQjJa06P8KfCPWZQ0NwZbPQnbQi9OWSZhFVSUWjjIrn4hSw==} engines: {node: '>=12'} @@ -1192,15 +1016,6 @@ packages: dev: true optional: true - /@esbuild/win32-x64@0.18.14: - resolution: {integrity: sha512-K0QjGbcskx+gY+qp3v4/940qg8JitpXbdxFhRDA1aYoNaPff88+aEwoq45aqJ+ogpxQxmU0ZTjgnrQD/w8iiUg==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - /@esbuild/win32-x64@0.19.8: resolution: {integrity: sha512-bfZ0cQ1uZs2PqpulNL5j/3w+GDhP36k1K5c38QdQg+Swy51jFZWWeIkteNsufkQxp986wnqRRsb/bHbY1WQ7TA==} engines: {node: '>=12'} @@ -1230,7 +1045,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) espree: 9.6.1 globals: 13.23.0 ignore: 5.3.0 @@ -1257,7 +1072,7 @@ packages: engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 2.0.1 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -1298,7 +1113,7 @@ packages: '@antfu/install-pkg': 0.1.1 '@antfu/utils': 0.7.6 '@iconify/types': 2.0.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) kolorist: 1.8.0 local-pkg: 0.4.3 transitivePeerDependencies: @@ -1414,20 +1229,6 @@ packages: typescript: 5.3.2 dev: true - /@rollup/pluginutils@5.1.0: - resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - dependencies: - '@types/estree': 1.0.5 - estree-walker: 2.0.2 - picomatch: 2.3.1 - dev: true - /@rollup/pluginutils@5.1.0(rollup@4.6.1): resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} engines: {node: '>=14.0.0'} @@ -1585,7 +1386,7 @@ packages: vite: ^4.0.0 dependencies: '@sveltejs/vite-plugin-svelte': 2.5.3(svelte@4.2.8)(vite@5.0.6) - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) svelte: 4.2.8 vite: 5.0.6 transitivePeerDependencies: @@ -1601,7 +1402,7 @@ packages: vite: ^5.0.0 dependencies: '@sveltejs/vite-plugin-svelte': 3.0.1(svelte@4.2.8)(vite@5.0.6) - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) svelte: 4.2.8 vite: 5.0.6 transitivePeerDependencies: @@ -1616,7 +1417,7 @@ packages: vite: ^4.0.0 dependencies: '@sveltejs/vite-plugin-svelte-inspector': 1.0.4(@sveltejs/vite-plugin-svelte@2.5.3)(svelte@4.2.8)(vite@5.0.6) - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.5 @@ -1636,7 +1437,7 @@ packages: vite: ^5.0.0 dependencies: '@sveltejs/vite-plugin-svelte-inspector': 2.0.0(@sveltejs/vite-plugin-svelte@3.0.1)(svelte@4.2.8)(vite@5.0.6) - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.5 @@ -1818,7 +1619,7 @@ packages: '@typescript-eslint/type-utils': 6.13.2(eslint@8.55.0)(typescript@5.3.2) '@typescript-eslint/utils': 6.13.2(eslint@8.55.0)(typescript@5.3.2) '@typescript-eslint/visitor-keys': 6.13.2 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) eslint: 8.55.0 graphemer: 1.4.0 ignore: 5.3.0 @@ -1844,7 +1645,7 @@ packages: '@typescript-eslint/types': 6.13.2 '@typescript-eslint/typescript-estree': 6.13.2(typescript@5.3.2) '@typescript-eslint/visitor-keys': 6.13.2 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) eslint: 8.55.0 typescript: 5.3.2 transitivePeerDependencies: @@ -1871,7 +1672,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 6.13.2(typescript@5.3.2) '@typescript-eslint/utils': 6.13.2(eslint@8.55.0)(typescript@5.3.2) - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) eslint: 8.55.0 ts-api-utils: 1.0.3(typescript@5.3.2) typescript: 5.3.2 @@ -1895,7 +1696,7 @@ packages: dependencies: '@typescript-eslint/types': 6.13.2 '@typescript-eslint/visitor-keys': 6.13.2 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 @@ -1936,7 +1737,7 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true - /@unocss/astro@0.58.0(vite@5.0.6): + /@unocss/astro@0.58.0(rollup@4.6.1)(vite@5.0.6): resolution: {integrity: sha512-df+tEFO5eKXjQOwSWQhS9IdjD0sfLHLtn8U09sEKR2Nmh5CvpwyBxmvLQgOCilPou7ehmyKfsyGRLZg7IMp+Ew==} peerDependencies: vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 @@ -1946,19 +1747,19 @@ packages: dependencies: '@unocss/core': 0.58.0 '@unocss/reset': 0.58.0 - '@unocss/vite': 0.58.0(vite@5.0.6) + '@unocss/vite': 0.58.0(rollup@4.6.1)(vite@5.0.6) vite: 5.0.6 transitivePeerDependencies: - rollup dev: true - /@unocss/cli@0.58.0: + /@unocss/cli@0.58.0(rollup@4.6.1): resolution: {integrity: sha512-rhsrDBxAVueygMcAbMkbuvsHbBL2rG6N96LllYwHn16FLgOE3Sf4JW1/LlNjQje3BtwMMtbSCCAeu2SryFhzbw==} engines: {node: '>=14'} hasBin: true dependencies: '@ampproject/remapping': 2.2.1 - '@rollup/pluginutils': 5.1.0 + '@rollup/pluginutils': 5.1.0(rollup@4.6.1) '@unocss/config': 0.58.0 '@unocss/core': 0.58.0 '@unocss/preset-uno': 0.58.0 @@ -2134,13 +1935,13 @@ packages: '@unocss/core': 0.58.0 dev: true - /@unocss/vite@0.58.0(vite@5.0.6): + /@unocss/vite@0.58.0(rollup@4.6.1)(vite@5.0.6): resolution: {integrity: sha512-OCUOLMSOBEtXOEyBbAvMI3/xdR175BWRzmvV9Wc34ANZclEvCdVH8+WU725ibjY4VT0gVIuX68b13fhXdHV41A==} peerDependencies: vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 dependencies: '@ampproject/remapping': 2.2.1 - '@rollup/pluginutils': 5.1.0 + '@rollup/pluginutils': 5.1.0(rollup@4.6.1) '@unocss/config': 0.58.0 '@unocss/core': 0.58.0 '@unocss/inspector': 0.58.0 @@ -2477,12 +2278,12 @@ packages: ip-regex: 5.0.0 dev: true - /cidr-tools@6.4.1: - resolution: {integrity: sha512-s8JNDwWgc2e0roEF6KDkQfHkZgEnehoap5hK7swPlEQMb9f8msrWqpgVCVKiDm3ARxpesOru9Tu49N8UpJjmDA==} + /cidr-tools@6.4.2: + resolution: {integrity: sha512-KZC8t2ipCqU2M+ISmTxRDGu9bku5MRU3V1cWyGEFJTZEzRhGvBJvVsbpZO5UAu12fExRFihtYGXAlgFFpmK9jw==} engines: {node: '>=16'} dependencies: cidr-regex: 4.0.3 - ip-bigint: 7.2.1 + ip-bigint: 7.3.0 ip-regex: 5.0.0 string-natural-compare: 3.0.1 dev: true @@ -2652,18 +2453,6 @@ packages: ms: 2.1.3 dev: true - /debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.2 - dev: true - /debug@4.3.4(supports-color@8.1.1): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -2695,7 +2484,7 @@ packages: resolution: {integrity: sha512-AD7TrdNNPXRZIGw63dw+lnGmT4v7ggZC5NHNJgAYWm5njrwoze1q5JSAW9YuLy2tjnoLUG/r8FEB93MCh9QJPg==} engines: {node: '>= 16'} dependencies: - execa: 7.1.1 + execa: 7.2.0 dev: true /defaults@1.0.4: @@ -2773,18 +2562,6 @@ packages: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} dev: true - /effection@2.0.8: - resolution: {integrity: sha512-/v7cbPIXGGylInQgHHjJutzqUn6VIfcP13hh2X0hXf04wwAlSI+lVjUBKpr5TX3+v9dXV/JLHO/pqQ9Cp1QAnQ==} - dependencies: - '@effection/channel': 2.0.6 - '@effection/core': 2.2.3 - '@effection/events': 2.0.6 - '@effection/fetch': 2.0.7(mocha@10.2.0) - '@effection/main': 2.1.2 - '@effection/stream': 2.0.6 - '@effection/subscription': 2.0.6 - dev: true - /effection@2.0.8(mocha@10.2.0): resolution: {integrity: sha512-/v7cbPIXGGylInQgHHjJutzqUn6VIfcP13hh2X0hXf04wwAlSI+lVjUBKpr5TX3+v9dXV/JLHO/pqQ9Cp1QAnQ==} dependencies: @@ -2881,36 +2658,6 @@ packages: resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} dev: true - /esbuild@0.18.14: - resolution: {integrity: sha512-uNPj5oHPYmj+ZhSQeYQVFZ+hAlJZbAGOmmILWIqrGvPVlNLbyOvU5Bu6Woi8G8nskcx0vwY0iFoMPrzT86Ko+w==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/android-arm': 0.18.14 - '@esbuild/android-arm64': 0.18.14 - '@esbuild/android-x64': 0.18.14 - '@esbuild/darwin-arm64': 0.18.14 - '@esbuild/darwin-x64': 0.18.14 - '@esbuild/freebsd-arm64': 0.18.14 - '@esbuild/freebsd-x64': 0.18.14 - '@esbuild/linux-arm': 0.18.14 - '@esbuild/linux-arm64': 0.18.14 - '@esbuild/linux-ia32': 0.18.14 - '@esbuild/linux-loong64': 0.18.14 - '@esbuild/linux-mips64el': 0.18.14 - '@esbuild/linux-ppc64': 0.18.14 - '@esbuild/linux-riscv64': 0.18.14 - '@esbuild/linux-s390x': 0.18.14 - '@esbuild/linux-x64': 0.18.14 - '@esbuild/netbsd-x64': 0.18.14 - '@esbuild/openbsd-x64': 0.18.14 - '@esbuild/sunos-x64': 0.18.14 - '@esbuild/win32-arm64': 0.18.14 - '@esbuild/win32-ia32': 0.18.14 - '@esbuild/win32-x64': 0.18.14 - dev: true - /esbuild@0.19.8: resolution: {integrity: sha512-l7iffQpT2OrZfH2rXIp7/FkmaeZM0vxbxN9KfiCwGYuZqzMg/JdvX26R31Zxn/Pxvsrg3Y9N6XTcnknqDyyv4w==} engines: {node: '>=12'} @@ -3160,7 +2907,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -3257,8 +3004,8 @@ packages: strip-final-newline: 2.0.0 dev: true - /execa@7.1.1: - resolution: {integrity: sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==} + /execa@7.2.0: + resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==} engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} dependencies: cross-spawn: 7.0.3 @@ -3675,7 +3422,7 @@ packages: resolution: {integrity: sha512-e6c3zxr9COnnc29PIz9LffmALOt0XhIJdR7f83DyHcQksL3B40KGmU3Sr1lrHja3i7Zyqo+AbwKZ+nZiMvg/OA==} engines: {node: '>=16'} dependencies: - cidr-tools: 6.4.1 + cidr-tools: 6.4.2 default-gateway: 7.2.2 is-ip: 5.0.0 p-event: 5.0.1 @@ -3690,8 +3437,8 @@ packages: side-channel: 1.0.4 dev: true - /ip-bigint@7.2.1: - resolution: {integrity: sha512-AftDIrlM5ZQM+qQ31IQ5MsL3tJWleeN3r0VqhmkB9oLvwcaDLeLNPtX4d9hahzExTFtz69eRv6LsGAoH20/8/g==} + /ip-bigint@7.3.0: + resolution: {integrity: sha512-2qVAe0Q9+Y+5nGvmogwK9y4kefD5Ks5l/IG0Jo1lhU9gIF34jifhqrwXwzkIl+LC594Q6SyAlngs4p890xsXVw==} engines: {node: '>=16'} dev: true @@ -4102,7 +3849,7 @@ packages: /micromark@2.11.4: resolution: {integrity: sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==} dependencies: - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) parse-entities: 2.0.0 transitivePeerDependencies: - supports-color @@ -4621,14 +4368,6 @@ packages: glob: 7.2.3 dev: true - /rollup@3.29.4: - resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==} - engines: {node: '>=14.18.0', npm: '>=8.0.0'} - hasBin: true - optionalDependencies: - fsevents: 2.3.3 - dev: true - /rollup@4.6.1: resolution: {integrity: sha512-jZHaZotEHQaHLgKr8JnQiDT1rmatjgKlMekyksz+yk9jt/8z9quNjnKNRoaM0wd9DC2QKXjmWWuDYtM3jfF8pQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -4663,7 +4402,7 @@ packages: /rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} dependencies: - tslib: 2.6.2 + tslib: 2.6.0 dev: true /sade@1.8.1: @@ -5133,8 +4872,8 @@ packages: strip-bom: 3.0.0 dev: true - /tslib@2.6.2: - resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + /tslib@2.6.0: + resolution: {integrity: sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==} dev: true /type-check@0.4.0: @@ -5250,7 +4989,7 @@ packages: '@types/unist': 2.0.10 dev: true - /unocss@0.58.0(postcss@8.4.32)(vite@5.0.6): + /unocss@0.58.0(postcss@8.4.32)(rollup@4.6.1)(vite@5.0.6): resolution: {integrity: sha512-MSPRHxBqWN+1AHGV+J5uUy4//e6ZBK6O+ISzD0qrXcCD/GNtxk1+lYjOK2ltkUiKX539+/KF91vNxzhhwEf+xA==} engines: {node: '>=14'} peerDependencies: @@ -5262,8 +5001,8 @@ packages: vite: optional: true dependencies: - '@unocss/astro': 0.58.0(vite@5.0.6) - '@unocss/cli': 0.58.0 + '@unocss/astro': 0.58.0(rollup@4.6.1)(vite@5.0.6) + '@unocss/cli': 0.58.0(rollup@4.6.1) '@unocss/core': 0.58.0 '@unocss/extractor-arbitrary-variants': 0.58.0 '@unocss/postcss': 0.58.0(postcss@8.4.32) @@ -5281,7 +5020,7 @@ packages: '@unocss/transformer-compile-class': 0.58.0 '@unocss/transformer-directives': 0.58.0 '@unocss/transformer-variant-group': 0.58.0 - '@unocss/vite': 0.58.0(vite@5.0.6) + '@unocss/vite': 0.58.0(rollup@4.6.1)(vite@5.0.6) vite: 5.0.6 transitivePeerDependencies: - postcss @@ -5326,41 +5065,6 @@ packages: vfile-message: 2.0.4 dev: true - /vite@4.5.0: - resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==} - engines: {node: ^14.18.0 || >=16.0.0} - hasBin: true - peerDependencies: - '@types/node': '>= 14' - less: '*' - lightningcss: ^1.21.0 - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - dependencies: - esbuild: 0.18.14 - postcss: 8.4.32 - rollup: 3.29.4 - optionalDependencies: - fsevents: 2.3.3 - dev: true - /vite@5.0.6: resolution: {integrity: sha512-MD3joyAEBtV7QZPl2JVVUai6zHms3YOmLR+BpMzLlX2Yzjfcc4gTgNi09d/Rua3F4EtC8zdwPU8eQYyib4vVMQ==} engines: {node: ^18.0.0 || >=20.0.0}