From 972bd9802bc1be319e35d902065c603b2b2ba0d2 Mon Sep 17 00:00:00 2001 From: SrGooglo Date: Mon, 19 Jun 2023 19:21:25 +0000 Subject: [PATCH] added `tap_share` settings --- .../constants/settings/tap_share/context.js | 9 + .../constants/settings/tap_share/errors.js | 11 + .../constants/settings/tap_share/index.jsx | 301 ++++++++++++++++++ .../constants/settings/tap_share/index.less | 281 ++++++++++++++++ .../tap_share/steps/check_register/index.jsx | 116 +++++++ .../tap_share/steps/data_editor/index.jsx | 180 +++++++++++ .../tap_share/steps/success/index.jsx | 26 ++ .../tap_share/steps/tag_writter/index.jsx | 110 +++++++ 8 files changed, 1034 insertions(+) create mode 100644 packages/app/constants/settings/tap_share/context.js create mode 100644 packages/app/constants/settings/tap_share/errors.js create mode 100644 packages/app/constants/settings/tap_share/index.jsx create mode 100644 packages/app/constants/settings/tap_share/index.less create mode 100644 packages/app/constants/settings/tap_share/steps/check_register/index.jsx create mode 100644 packages/app/constants/settings/tap_share/steps/data_editor/index.jsx create mode 100644 packages/app/constants/settings/tap_share/steps/success/index.jsx create mode 100644 packages/app/constants/settings/tap_share/steps/tag_writter/index.jsx diff --git a/packages/app/constants/settings/tap_share/context.js b/packages/app/constants/settings/tap_share/context.js new file mode 100644 index 00000000..dc643fe0 --- /dev/null +++ b/packages/app/constants/settings/tap_share/context.js @@ -0,0 +1,9 @@ +import React from "react" + +const RegisterNewTagStepsDefaultContext = { + next: () => { }, + prev: () => { }, + submit: () => { }, +} + +export default React.createContext(RegisterNewTagStepsDefaultContext) \ No newline at end of file diff --git a/packages/app/constants/settings/tap_share/errors.js b/packages/app/constants/settings/tap_share/errors.js new file mode 100644 index 00000000..d6d92179 --- /dev/null +++ b/packages/app/constants/settings/tap_share/errors.js @@ -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.", +} diff --git a/packages/app/constants/settings/tap_share/index.jsx b/packages/app/constants/settings/tap_share/index.jsx new file mode 100644 index 00000000..735f92a7 --- /dev/null +++ b/packages/app/constants/settings/tap_share/index.jsx @@ -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
+
+ + + +
+
+ } + + if (app.cores.nfc.incompatible) { + return + } + + return
0 + } + )} + > +
+ } + className={classnames( + "tap-share-register-header-back", + { + ["hidden"]: step === 0 + } + )} + /> + +
+ +
+ +

+ Register new tag +

+
+ +
+ + { + React.createElement(RegisterNewTagSteps[step]) + } + +
+
+} + +const TagItem = (props) => { + return
+
+ +
+ +
+

+ {props.tag.alias} +

+ + + {props.tag.serial} + +
+ +
+ } + onClick={props.onEdit} + /> + } + danger + disabled + /> +
+
+} + +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
+ +
+ } + + return
+ { + this.state.data.length === 0 && + } + + { + this.state.data.length > 0 && this.state.data.map((tag) => { + return { + OpenTagEditor({ + tag + }) + }} + /> + }) + } +
+ } +} + +const OpenTagEditor = ({ tag, onFinish = () => app.navigation.softReload() } = {}) => { + app.DrawerController.open("tag_register", RegisterNewTag, { + componentProps: { + tagData: tag, + onFinish: onFinish, + } + }) +} + +const TapShareRender = () => { + return
+
+
+

+ Registered Tags +

+
+ + You can quickly edit your tags by tapping them. + + + +
+ + } + onClick={() => OpenTagEditor()} + > + Add new + +
+} + +export default { + id: "tap_share", + icon: "MdNfc", + label: "Tap Share", + group: "app", + render: TapShareRender +} \ No newline at end of file diff --git a/packages/app/constants/settings/tap_share/index.less b/packages/app/constants/settings/tap_share/index.less new file mode 100644 index 00000000..167b0de6 --- /dev/null +++ b/packages/app/constants/settings/tap_share/index.less @@ -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; + } + } +} \ No newline at end of file diff --git a/packages/app/constants/settings/tap_share/steps/check_register/index.jsx b/packages/app/constants/settings/tap_share/steps/check_register/index.jsx new file mode 100644 index 00000000..0d45fa38 --- /dev/null +++ b/packages/app/constants/settings/tap_share/steps/check_register/index.jsx @@ -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
+ + + { + error && + } + +

+ Tap your tag to your phone +

+ + { + loading && + } +
+} \ No newline at end of file diff --git a/packages/app/constants/settings/tap_share/steps/data_editor/index.jsx b/packages/app/constants/settings/tap_share/steps/data_editor/index.jsx new file mode 100644 index 00000000..cbc73a93 --- /dev/null +++ b/packages/app/constants/settings/tap_share/steps/data_editor/index.jsx @@ -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
+

+ Tag Data +

+ + + + + Serial + } + > + + + + + Alias + } + rules={[ + { + required: true, + message: "Please input an alias." + } + ]} + > + + + + + + Behavior + } + > + + What will happen when someone taps your tag? + + +
+ + + + Custom URL + + + + Profile + + + + Post + + + + + + + +
+
+ + + + Save + + +
+
+} \ No newline at end of file diff --git a/packages/app/constants/settings/tap_share/steps/success/index.jsx b/packages/app/constants/settings/tap_share/steps/success/index.jsx new file mode 100644 index 00000000..baa96164 --- /dev/null +++ b/packages/app/constants/settings/tap_share/steps/success/index.jsx @@ -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
+ +

+ Your tag is ready to use! +

+
+} \ No newline at end of file diff --git a/packages/app/constants/settings/tap_share/steps/tag_writter/index.jsx b/packages/app/constants/settings/tap_share/steps/tag_writter/index.jsx new file mode 100644 index 00000000..86cc261b --- /dev/null +++ b/packages/app/constants/settings/tap_share/steps/tag_writter/index.jsx @@ -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
+

+ Your tag is ready to write! +

+ +

+ Tap your tag to your phone to write the data. +
+ This is only necessary the first time you use your tag. +

+ + +

+ Please do not pick up the tag +

+ + + + { + error && + } +
+} \ No newline at end of file