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 <fabianlars@crabnebula.dev> Co-authored-by: Lucas Nogueira <lucas@tauri.app> Co-authored-by: Lucas Nogueira <lucas@tauri.studio>pull/829/head^2
parent
7c59e3785b
commit
fe79adb5c7
@ -0,0 +1,6 @@
|
||||
---
|
||||
"nfc": major
|
||||
"nfc-js": major
|
||||
---
|
||||
|
||||
Initial release.
|
@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="17" />
|
||||
</component>
|
||||
</project>
|
@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="GRADLE" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="jbr-17" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$USER_HOME$/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tauri-2.0.0-alpha.17/mobile/android" />
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="$PROJECT_DIR$/buildSrc" />
|
||||
<option value="$PROJECT_DIR$/../../../../../plugins/barcode-scanner/android" />
|
||||
<option value="$PROJECT_DIR$/../../../../../plugins/clipboard-manager/android" />
|
||||
<option value="$PROJECT_DIR$/../../../../../plugins/dialog/android" />
|
||||
<option value="$PROJECT_DIR$/../../../../../plugins/nfc/android" />
|
||||
<option value="$PROJECT_DIR$/../../../../../plugins/notification/android" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteRepositoriesConfiguration">
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central repository" />
|
||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="jboss.community" />
|
||||
<option name="name" value="JBoss Community repository" />
|
||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="MavenRepo" />
|
||||
<option name="name" value="MavenRepo" />
|
||||
<option name="url" value="https://repo.maven.apache.org/maven2/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="Google" />
|
||||
<option name="name" value="Google" />
|
||||
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.8.10" />
|
||||
</component>
|
||||
</project>
|
@ -0,0 +1,9 @@
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/../../../../.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
@ -1,5 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
<dict>
|
||||
<key>com.apple.developer.nfc.readersession.formats</key>
|
||||
<array>
|
||||
<string>TAG</string>
|
||||
<string>NDEF</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -0,0 +1,126 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { write, scan, textRecord, uriRecord } from "@tauri-apps/plugin-nfc";
|
||||
import * as os from "@tauri-apps/plugin-os";
|
||||
|
||||
export let onMessage;
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
let kind = "tag";
|
||||
let writeToNfc = false;
|
||||
let text = "";
|
||||
let uri = "";
|
||||
|
||||
let scheme = "";
|
||||
let host = "";
|
||||
let pathPrefix = "";
|
||||
let mimeType = "";
|
||||
|
||||
let isAndroid;
|
||||
onMount(async () => {
|
||||
isAndroid = (await os.platform()) === "android";
|
||||
});
|
||||
|
||||
async function _readNfc() {
|
||||
onMessage(`NFC scanning ${kind}`);
|
||||
|
||||
const tagResponse = await scan(
|
||||
{
|
||||
type: kind,
|
||||
uri: {
|
||||
scheme: scheme || null,
|
||||
host: host || null,
|
||||
pathPrefix: pathPrefix || null,
|
||||
},
|
||||
mimeType: mimeType || null,
|
||||
},
|
||||
{
|
||||
keepSessionAlive: writeToNfc,
|
||||
message: "Hold your iPhone near an NFC tag",
|
||||
successMessage: "Tag successfully read",
|
||||
}
|
||||
);
|
||||
|
||||
onMessage({
|
||||
id: decoder.decode(new Uint8Array(tagResponse.id)),
|
||||
kind: tagResponse.kind,
|
||||
records: tagResponse.records.map((record) => ({
|
||||
id: decoder.decode(new Uint8Array(record.id)),
|
||||
kind: decoder.decode(new Uint8Array(record.kind)),
|
||||
payload: decoder.decode(new Uint8Array(record.payload)),
|
||||
tnf: record.tnf,
|
||||
})),
|
||||
});
|
||||
|
||||
if (writeToNfc) {
|
||||
const records = [];
|
||||
if (text) {
|
||||
records.push(textRecord(text, "tauriTextId"));
|
||||
}
|
||||
if (uri) {
|
||||
records.push(uriRecord(uri, "tauriUriId"));
|
||||
}
|
||||
await write(records, {
|
||||
successMessage: "Data written to tag",
|
||||
});
|
||||
onMessage("Wrote to tag");
|
||||
}
|
||||
}
|
||||
|
||||
function readNfc() {
|
||||
_readNfc().catch(onMessage);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="flex gap-2 children:grow items-center">
|
||||
<div>
|
||||
<input type="checkbox" id="nfc-write" bind:checked={writeToNfc} />
|
||||
<label for="nfc-write">Write data</label>
|
||||
</div>
|
||||
|
||||
<select class="input" id="request-method" bind:value={kind}>
|
||||
<option value="tag">TAG</option>
|
||||
<option value="ndef">NDEF</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{#if isAndroid}
|
||||
<div class="flex flex-col gap-2 children:grow">
|
||||
<p>Filters</p>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
class="input"
|
||||
placeholder="Scheme"
|
||||
style="width: 33%"
|
||||
bind:value={scheme}
|
||||
/>
|
||||
<input
|
||||
class="input"
|
||||
placeholder="Host"
|
||||
style="width: 33%"
|
||||
bind:value={host}
|
||||
/>
|
||||
<input
|
||||
class="input"
|
||||
placeholder="Path prefix"
|
||||
style="width: 33%"
|
||||
bind:value={pathPrefix}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<input class="input" placeholder="Mime type" bind:value={mimeType} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col gap-2 children:grow">
|
||||
<p>Write Records</p>
|
||||
<div class="flex">
|
||||
<input class="input" placeholder="Text record" bind:value={text} />
|
||||
<input class="input" placeholder="URI record" bind:value={uri} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn" on:click={readNfc}>Scan</button>
|
||||
</div>
|
@ -0,0 +1 @@
|
||||
/.tauri
|
@ -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"
|
@ -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: <text>Tauri is a rust project that enables developers to make secure
|
||||
and small desktop applications using a web frontend.
|
||||
</text>
|
||||
PackageComment: <text>The package includes the following libraries; see
|
||||
Relationship information.
|
||||
</text>
|
||||
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
|
@ -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
|
@ -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.
|
@ -0,0 +1,113 @@
|
||||

|
||||
|
||||
<!-- description -->
|
||||
|
||||
## 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.
|
||||
|
||||
<!-- Add the branch for installations using git! -->
|
||||
|
||||
```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
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://crabnebula.dev" target="_blank">
|
||||
<img src="contributors/crabnebula.svg" alt="CrabNebula" width="283">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://impierce.com" target="_blank">
|
||||
<img src="contributors/impierce.svg" alt="Impierce" width="283" height="90">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Partners
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://crabnebula.dev" target="_blank">
|
||||
<img src="https://github.com/tauri-apps/plugins-workspace/raw/v2/.github/sponsors/crabnebula.svg" alt="CrabNebula" width="283">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
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.
|
@ -0,0 +1,2 @@
|
||||
/build
|
||||
/.tauri
|
@ -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"))
|
||||
}
|
@ -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
|
@ -0,0 +1,2 @@
|
||||
include ':tauri-android'
|
||||
project(':tauri-android').projectDir = new File('./.tauri/tauri-api')
|
@ -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)
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
<!-- TODO: if the app doesn't rely on nfc the uses-feature line could be removed. The app store uses this prop to decide whether or not to show the app. -->
|
||||
<!-- TODO: Move that info somewhere else since users usually can't see this file. -->
|
||||
<!-- Paste the following line into the AndroidManifest.xml of your project -->
|
||||
<!-- See also: https://capacitorjs.com/docs/plugins/android#manifest -->
|
||||
<!-- <uses-feature android:name="android.hardware.nfc" android:required="true" /> -->
|
||||
</manifest>
|
@ -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<Array<TechKind>>? = null
|
||||
}
|
||||
|
||||
fun filters(): Array<IntentFilter>? {
|
||||
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<Array<String>>? {
|
||||
return when (this) {
|
||||
is Tag -> null
|
||||
is Ndef -> {
|
||||
techLists?.let {
|
||||
val techs = mutableListOf<Array<String>>()
|
||||
for (techList in it) {
|
||||
val list = mutableListOf<String>()
|
||||
for (tech in techList) {
|
||||
list.add(tech.className())
|
||||
}
|
||||
techs.add(list.toTypedArray())
|
||||
}
|
||||
techs.toTypedArray()
|
||||
} ?: run {
|
||||
null
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class ScanKindDeserializer: JsonDeserializer<ScanKind>() {
|
||||
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<NDEFRecordData>
|
||||
}
|
||||
|
||||
class Session(
|
||||
val action: NfcAction,
|
||||
val invoke: Invoke,
|
||||
val keepAlive: Boolean,
|
||||
var tag: Tag? = null,
|
||||
val filters: Array<IntentFilter>? = null,
|
||||
val techLists: Array<Array<String>>? = 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<Boolean, String?> {
|
||||
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<NdefRecord> = 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<Parcelable>?) {
|
||||
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<IntentFilter>?, techLists: Array<Array<String>>?) {
|
||||
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
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<tech-list>
|
||||
<tech>android.nfc.tech.IsoDep</tech>
|
||||
<tech>android.nfc.tech.NfcA</tech>
|
||||
<tech>android.nfc.tech.NfcB</tech>
|
||||
<tech>android.nfc.tech.NfcF</tech>
|
||||
<tech>android.nfc.tech.NfcV</tech>
|
||||
<tech>android.nfc.tech.Ndef</tech>
|
||||
<tech>android.nfc.tech.NdefFormatable</tech>
|
||||
<tech>android.nfc.tech.MifareClassic</tech>
|
||||
<tech>android.nfc.tech.MifareUltralight</tech>
|
||||
</tech-list>
|
||||
</resources>
|
@ -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)
|
||||
}
|
||||
}
|
@ -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#"<intent-filter>
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.nfc.action.TECH_DISCOVERED" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.nfc.action.TAG_DISCOVERED" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.nfc.action.TECH_DISCOVERED"
|
||||
android:resource="@xml/nfc_tech_filter" />"#
|
||||
.to_string(),
|
||||
)
|
||||
.expect("failed to rewrite AndroidManifest.xml");
|
||||
}
|
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 14 KiB |
@ -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 <https://developer.android.com/reference/android/nfc/NfcAdapter#ACTION_TECH_DISCOVERED> 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<string, unknown> {
|
||||
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 <https://developer.android.com/develop/connectivity/nfc/nfc#ndef> for more information.
|
||||
*
|
||||
* @param kind
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
export async function scan(
|
||||
kind: ScanKind,
|
||||
options?: ScanOptions,
|
||||
): Promise<Tag> {
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
return await invoke("plugin:nfc|isAvailable");
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/config/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
Package.resolved
|
@ -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")
|
||||
]
|
||||
)
|
@ -0,0 +1,3 @@
|
||||
# Tauri Plugin Nfc
|
||||
|
||||
A description of this package.
|
@ -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<T: NFCNDEFTag>(
|
||||
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<T: NFCNDEFTag>(
|
||||
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<T: NFCNDEFTag>(
|
||||
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()
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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();
|
@ -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__})}
|
@ -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<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[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<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
@ -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<R: Runtime>(PluginHandle<R>);
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct IsAvailableResponse {
|
||||
available: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct WriteRequest {
|
||||
records: Vec<NfcRecord>,
|
||||
}
|
||||
|
||||
impl<R: Runtime> Nfc<R> {
|
||||
pub fn is_available(&self) -> crate::Result<bool> {
|
||||
self.0
|
||||
.run_mobile_plugin::<IsAvailableResponse>("isAvailable", ())
|
||||
.map(|r| r.available)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn scan(&self, payload: ScanRequest) -> crate::Result<ScanResponse> {
|
||||
self.0
|
||||
.run_mobile_plugin("scan", payload)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn write(&self, records: Vec<NfcRecord>) -> 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<R: Runtime> {
|
||||
fn nfc(&self) -> &Nfc<R>;
|
||||
}
|
||||
|
||||
impl<R: Runtime, T: Manager<R>> crate::NfcExt<R> for T {
|
||||
fn nfc(&self) -> &Nfc<R> {
|
||||
self.state::<Nfc<R>>().inner()
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the plugin.
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
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()
|
||||
}
|
@ -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::*;
|
@ -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<u8>,
|
||||
pub id: Vec<u8>,
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
#[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<u8>,
|
||||
pub id: Vec<u8>,
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct NfcTag {
|
||||
pub id: String,
|
||||
pub kind: String,
|
||||
pub records: Vec<NfcTagRecord>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ScanResponse {
|
||||
pub tag: NfcTag,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct UriFilter {
|
||||
scheme: Option<String>,
|
||||
host: Option<String>,
|
||||
path_prefix: Option<String>,
|
||||
}
|
||||
|
||||
#[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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ScanKind {
|
||||
Ndef {
|
||||
mime_type: Option<String>,
|
||||
uri: Option<UriFilter>,
|
||||
tech_list: Option<Vec<Vec<TechKind>>>,
|
||||
},
|
||||
Tag {
|
||||
mime_type: Option<String>,
|
||||
uri: Option<UriFilter>,
|
||||
},
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": ["guest-js/*.ts"]
|
||||
}
|
Loading…
Reference in new issue