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"?>
|
<?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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict/>
|
<dict>
|
||||||
|
<key>com.apple.developer.nfc.readersession.formats</key>
|
||||||
|
<array>
|
||||||
|
<string>TAG</string>
|
||||||
|
<string>NDEF</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
</plist>
|
</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