added tap_share settings

This commit is contained in:
SrGooglo 2023-06-19 19:21:25 +00:00
parent 02db8c6c13
commit 972bd9802b
8 changed files with 1034 additions and 0 deletions

View File

@ -0,0 +1,9 @@
import React from "react"
const RegisterNewTagStepsDefaultContext = {
next: () => { },
prev: () => { },
submit: () => { },
}
export default React.createContext(RegisterNewTagStepsDefaultContext)

View File

@ -0,0 +1,11 @@
export default {
NFC_NOT_SUPPORTED: "Your device doesn't support NFC.",
NFC_NOT_ENABLED: "NFC is not enabled.",
NFC_NOT_READABLE: "NFC is not readable.",
NFC_NOT_WRITABLE: "NFC is not writable.",
NFC_READ_ERROR: "Cannot read NFC tag. Please try again.",
NFC_WRITE_ERROR: "Cannot write NFC tag. Please try again.",
NFC_NOT_OWNER: "This tag is not owned by you.",
NFC_NOT_REGISTERED: "This tag is not registered.",
NFC_NOT_MATCH: "This tag is not match with the registered tag.",
}

View File

@ -0,0 +1,301 @@
import React from "react"
import * as antd from "antd"
import { Input } from "antd-mobile"
import classnames from "classnames"
import NFCModel from "comty.js/models/nfc"
import { Icons } from "components/Icons"
import AnimationPlayer from "components/AnimationPlayer"
import StepsContext from "./context"
import NFC_ERRORS from "./errors"
import "./index.less"
import CheckRegister from "./steps/check_register"
import DataEditor from "./steps/data_editor"
import TagWritter from "./steps/tag_writter"
import Success from "./steps/success"
const RegisterNewTagSteps = [
CheckRegister,
DataEditor,
TagWritter,
Success,
]
const RegisterNewTag = (props) => {
const [step, setStep] = React.useState(0)
const [stepsValues, setStepsValues] = React.useState({
...props.tagData ?? {}
})
const nextStep = () => {
setStep((step) => step + 1)
}
const prevStep = () => {
setStep((step) => step - 1)
}
const finish = () => {
if (typeof props.onFinish === "function") {
props.onFinish()
}
if (typeof props.close === "function") {
props.close()
}
}
// create a react context for the steps
const StepsContextValue = {
next: nextStep,
prev: prevStep,
values: stepsValues,
setValue: (key, value) => {
setStepsValues((stepsValues) => {
return {
...stepsValues,
[key]: value
}
})
},
onFinish: finish,
nfcReader: app.cores.nfc.instance(),
close: props.close
}
if (props.tagData) {
return <div className="tap-share-register">
<div className="tap-share-register-content">
<StepsContext.Provider value={StepsContextValue}>
<DataEditor
onFinish={finish}
/>
</StepsContext.Provider>
</div>
</div>
}
if (app.cores.nfc.incompatible) {
return <antd.Result
status="error"
title="Error"
subTitle="Your device doesn't support NFC."
/>
}
return <div
className={classnames(
"tap-share-register",
{
["compact"]: step > 0
}
)}
>
<div className="tap-share-register-header">
<antd.Button
type="link"
onClick={prevStep}
disabled={step === 0}
icon={<Icons.MdChevronLeft />}
className={classnames(
"tap-share-register-header-back",
{
["hidden"]: step === 0
}
)}
/>
<div className="tap-share-register-header-icon">
<Icons.MdNfc />
</div>
<h1>
Register new tag
</h1>
</div>
<div className="tap-share-register-content">
<StepsContext.Provider value={StepsContextValue}>
{
React.createElement(RegisterNewTagSteps[step])
}
</StepsContext.Provider>
</div>
</div>
}
const TagItem = (props) => {
return <div
key={props.tag.serialNumber}
id={props.tag.serialNumber}
className="tap-share-own_tags-item"
>
<div className="tap-share-own_tags-item-icon">
<Icons.MdNfc />
</div>
<div className="tap-share-own_tags-item-title">
<h4>
{props.tag.alias}
</h4>
<span>
{props.tag.serial}
</span>
</div>
<div className="tap-share-own_tags-item-actions">
<antd.Button
icon={<Icons.MdEdit />}
onClick={props.onEdit}
/>
<antd.Button
icon={<Icons.MdDelete />}
danger
disabled
/>
</div>
</div>
}
class OwnTags extends React.Component {
state = {
loading: true,
error: null,
data: null,
}
loadData = async () => {
this.setState({
loading: true,
})
const result = await NFCModel.getOwnTags()
.catch((err) => {
console.error(err)
this.setState({
error: err.message,
loading: false,
data: null
})
return false
})
if (!result) {
return false
}
this.setState({
loading: false,
data: result,
error: null
})
}
handleTagRead = async (error, tag) => {
if (error) {
console.error(error)
return false
}
const ownedTag = this.state.data.find((ownedTag) => {
return ownedTag.serial === tag.serialNumber
})
console.log(ownedTag)
if (!ownedTag) {
app.message.error("This tag is not registered or you don't have permission to edit it.")
return false
}
return OpenTagEditor({
tag: ownedTag
})
}
componentDidMount = async () => {
await this.loadData()
app.cores.nfc.subscribe(this.handleTagRead)
}
componentWillUnmount = () => {
app.cores.nfc.unsubscribe(this.handleTagRead)
}
render() {
if (this.state.loading) {
return <div className="tap-share-own_tags">
<antd.Skeleton />
</div>
}
return <div className="tap-share-own_tags">
{
this.state.data.length === 0 && <antd.Empty
description="You don't have any tags yet."
/>
}
{
this.state.data.length > 0 && this.state.data.map((tag) => {
return <TagItem
key={tag.serialNumber}
tag={tag}
onEdit={() => {
OpenTagEditor({
tag
})
}}
/>
})
}
</div>
}
}
const OpenTagEditor = ({ tag, onFinish = () => app.navigation.softReload() } = {}) => {
app.DrawerController.open("tag_register", RegisterNewTag, {
componentProps: {
tagData: tag,
onFinish: onFinish,
}
})
}
const TapShareRender = () => {
return <div className="tap-share-render">
<div className="tap-share-field">
<div className="tap-share-field_header">
<h1>
<Icons.MdSpoke /> Registered Tags
</h1>
</div>
<span className="tip">
<Icons.MdInfo /> You can quickly edit your tags by tapping them.
</span>
<OwnTags />
</div>
<antd.Button
type="primary"
icon={<Icons.Plus />}
onClick={() => OpenTagEditor()}
>
Add new
</antd.Button>
</div>
}
export default {
id: "tap_share",
icon: "MdNfc",
label: "Tap Share",
group: "app",
render: TapShareRender
}

View File

@ -0,0 +1,281 @@
.tap-share-render {
display: flex;
flex-direction: column;
gap: 20px;
.tap-share-field {
display: flex;
flex-direction: column;
background-color: var(--background-color-accent);
padding: 20px;
border-radius: 12px;
gap: 10px;
span {
font-size: 0.8rem;
opacity: 0.8;
}
.tap-share-field_header {
display: flex;
flex-direction: row;
align-items: center;
h1,
p {
margin: 0;
}
gap: 10px;
}
}
}
.tap-share-own_tags {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
.tap-share-own_tags-item {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
gap: 10px;
padding: 10px;
background-color: var(--background-color-primary);
border-radius: 8px;
.tap-share-own_tags-item-icon {
font-size: 2rem;
}
.tap-share-own_tags-item-title {
display: flex;
flex-direction: column;
width: 100%;
gap: 10px;
color: var(--text-color);
h1,
h2,
h3,
h4,
h5,
h6,
p,
span {
margin: 0;
}
span {
font-size: 0.7rem;
opacity: 0.7px;
}
}
.tap-share-own_tags-item-actions {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
}
}
}
.tap-share-register {
display: flex;
flex-direction: column;
height: 100%;
gap: 20px;
&.compact {
.tap-share-register-header {
padding: 10px;
flex-direction: row;
justify-content: flex-start;
.tap-share-register-header-icon {
font-size: 0;
width: 0;
height: 0;
}
}
}
.tap-share-register-header {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
gap: 10px;
background-color: var(--background-color-accent);
border-radius: 12px;
transition: all 150ms ease-in-out;
h1 {
margin: 0;
}
.tap-share-register-header-back {
font-size: 5rem;
color: var(--colorPrimary);
&.hidden {
width: 0;
height: 0;
padding: 0;
}
}
.tap-share-register-header-icon {
font-size: 5rem;
color: var(--colorPrimary);
svg {
margin: 0;
}
}
}
.tap-share-register-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
.tap-share-register_step {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
height: 100%;
width: 100%;
padding: 0 10px;
transition: all 150ms ease-in-out;
h1 {
transition: all 150ms ease-in-out;
}
&.centered {
align-items: center;
}
.ant-form {
width: 100%;
.ant-form-item {
height: fit-content;
padding: 0;
.ant-form_with_selector {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
.ant-select {
width: fit-content;
}
.ant-input {
width: 100%;
}
}
.ant-form-item-label {
height: fit-content;
padding: 0;
margin: 0;
label {
height: fit-content;
padding: 0;
margin: 0;
}
}
.description {
font-size: 0.8rem;
opacity: 0.8;
}
}
}
.tap-share-register_step_actions {
display: flex;
flex-direction: row;
align-items: center;
gap: 20px;
padding: 10px;
}
}
}
}
.animation-player {
width: 50%;
&.loading {
.phone-loop {
color: #17b2ff;
}
}
.phone-loop {
color: var(--colorPrimary);
path {
fill: currentColor;
stroke: currentColor;
}
}
}

View File

@ -0,0 +1,116 @@
import React from "react"
import * as antd from "antd"
import NFCModel from "comty.js/models/nfc"
import AnimationPlayer from "components/AnimationPlayer"
import NFC_ERRORS from "../../errors"
import StepsContext from "../../context"
export default (props) => {
const [error, setError] = React.useState(null)
const [loading, setLoading] = React.useState(false)
const context = React.useContext(StepsContext)
const readTagRegister = async (error, data) => {
if (error) {
console.error(error)
setError(NFC_ERRORS.NFC_READ_ERROR)
return false
}
if (!data) {
return false
}
setError(null)
setLoading(true)
console.log(data)
const registerResult = await NFCModel.getTagBySerial(data.serialNumber).catch((err) => {
if (err.response.status === 404) {
return false
}
return {
error: err,
is_owner: false
}
})
console.log(registerResult)
setLoading(false)
if (!registerResult) {
// this means that the tag is not registered, step to the next step
context.setValue("serial", data.serialNumber)
unregisterScan()
return context.next()
} else {
if (registerResult.error) {
return setError("Cannot check if the tag is registered. Please try again.")
}
if (!registerResult.is_owner) {
// this means that the tag is registered but not owned by the user
return setError(NFC_ERRORS.NFC_NOT_OWNER)
}
context.setValue("serial", data.serialNumber)
context.setValue("alias", registerResult.alias)
context.setValue("behavior", registerResult.behavior)
unregisterScan()
return context.next()
}
}
const unregisterScan = () => {
app.cores.nfc.unsubscribe(readTagRegister)
}
React.useEffect(() => {
app.cores.nfc.subscribe(readTagRegister)
return () => {
unregisterScan()
}
}, [])
return <div className="tap-share-register_step centered">
<AnimationPlayer
src="https://storage.ragestudio.net/comty-static-assets/animations/nfc_tap.json"
loop={true}
className={[
{
["loading"]: loading,
["error"]: error
}
]}
/>
{
error && <antd.Alert
type="error"
message={error}
/>
}
<h1 style={{
opacity: loading ? 0 : 1
}}>
Tap your tag to your phone
</h1>
{
loading && <antd.Spin />
}
</div>
}

View File

@ -0,0 +1,180 @@
import React from "react"
import * as antd from "antd"
import { Input } from "antd-mobile"
import NFCModel from "comty.js/models/nfc"
import { Icons } from "components/Icons"
import StepsContext from "../../context"
export default (props) => {
const context = React.useContext(StepsContext)
if (!context.values.serial) {
app.message.error("Serial not available.")
return <>
Serial not available, please try again.
</>
}
const handleOnFinish = async (values) => {
context.setValue("alias", values.alias)
context.setValue("behavior", values.behavior)
const result = await NFCModel.registerTag(context.values.serial, {
alias: values.alias,
behavior: values.behavior
}).catch((err) => {
console.error(err)
app.message.error("Cannot register your tag. Please try again.")
return false
})
if (!result) {
return false
}
if (!result.endpoint_url) {
app.message.error("Cannot register your tag. Please try again.")
return false
}
if (props.onFinish) {
app.message.success("All changes have been saved.")
return props.onFinish(result)
}
app.message.success("Your tag has been registered successfully.")
context.setValue("endpoint_url", result.endpoint_url)
return context.next()
}
return <div className="tap-share-register_step">
<h2>
Tag Data
</h2>
<antd.Form
name="register_tag"
onFinish={handleOnFinish}
initialValues={{
serial: context.values.serial,
alias: context.values.alias,
behavior: context.values.behavior,
}}
>
<antd.Form.Item
name="serial"
label={<>
<Icons.MdTag />
Serial
</>}
>
<Input
disabled
/>
</antd.Form.Item>
<antd.Form.Item
name="alias"
label={<>
<Icons.Tag />
Alias
</>}
rules={[
{
required: true,
message: "Please input an alias."
}
]}
>
<antd.Input
placeholder="Short name for your tag"
/>
</antd.Form.Item>
<antd.Form.Item
label={<>
<Icons.MdWebhook />
Behavior
</>}
>
<span className="description">
What will happen when someone taps your tag?
</span>
<div className="ant-form_with_selector">
<antd.Form.Item
name={["behavior", "type"]}
noStyle
size="large"
rules={[
{
required: true,
message: "Please select your tag behavior."
}
]}
>
<antd.Select
placeholder="Options"
size="large"
>
<antd.Select.Option
value="url"
>
Custom URL
</antd.Select.Option>
<antd.Select.Option
value="profile"
>
Profile
</antd.Select.Option>
<antd.Select.Option
value="post"
>
Post
</antd.Select.Option>
</antd.Select>
</antd.Form.Item>
<antd.Form.Item
name={["behavior", "value"]}
noStyle
rules={[
{
required: true,
message: "Please select your behavior value."
}
]}
>
<antd.Input
placeholder="value"
size="large"
autoCapitalize="off"
autoCorrect="off"
spellCheck="false"
/>
</antd.Form.Item>
</div>
</antd.Form.Item>
<antd.Form.Item
colon={false}
>
<antd.Button
block
type="primary"
size="large"
htmlType="submit"
>
Save
</antd.Button>
</antd.Form.Item>
</antd.Form>
</div>
}

View File

@ -0,0 +1,26 @@
import React from "react"
import AnimationPlayer from "components/AnimationPlayer"
import StepsContext from "../../context"
export default (props) => {
const context = React.useContext(StepsContext)
React.useEffect(() => {
setTimeout(() => {
if (typeof context.onFinish === "function") {
context.onFinish()
}
}, 2000)
}, [])
return <div className="tap-share-register_step centered">
<AnimationPlayer
src="https://assets10.lottiefiles.com/packages/lf20_dyy9le6w.json"
/>
<h1>
Your tag is ready to use!
</h1>
</div>
}

View File

@ -0,0 +1,110 @@
import React from "react"
import * as antd from "antd"
import AnimationPlayer from "components/AnimationPlayer"
import NFC_ERRORS from "../../errors"
import StepsContext from "../../context"
export default (props) => {
const context = React.useContext(StepsContext)
const [loading, setLoading] = React.useState(false)
const [error, setError] = React.useState(null)
const abortController = React.useRef(new AbortController())
const handleWritter = async (error, tag) => {
if (error) {
console.error(error)
setError(NFC_ERRORS.NFC_READ_ERROR)
return false
}
const nfcInstance = app.cores.nfc.instance()
if (!nfcInstance) {
setError(NFC_ERRORS.NFC_NOT_AVAILABLE)
return false
}
setError(null)
setLoading(true)
if (tag.serialNumber !== context.values.serial) {
setError(NFC_ERRORS.NFC_NOT_MATCH)
setLoading(false)
return false
}
nfcInstance.write({
records: [{
recordType: "url",
data: context.values.endpoint_url
}]
}, {
signal: abortController.current.signal
}).then(() => {
app.message.success("Tag written successfully.")
setLoading(false)
return context.next()
})
.catch((err) => {
console.error(err)
setError(NFC_ERRORS.NFC_WRITE_ERROR)
setLoading(false)
})
}
React.useEffect(() => {
app.cores.nfc.subscribe(handleWritter)
return () => {
app.cores.nfc.unsubscribe(handleWritter)
abortController.current.abort("finished")
}
}, [])
return <div className="tap-share-register_step centered">
<h1>
Your tag is ready to write!
</h1>
<p>
Tap your tag to your phone to write the data.
<br />
This is only necessary the first time you use your tag.
</p>
<h2 style={{
opacity: loading ? 1 : 0,
color: loading ? "red" : "inherit"
}}>
Please do not pick up the tag
</h2>
<AnimationPlayer
src="https://storage.ragestudio.net/comty-static-assets/animations/nfc_tap.json"
className={[
{
["loading"]: loading,
["error"]: error
}
]}
loop
/>
{
error && <antd.Alert
type="error"
message={error}
/>
}
</div>
}