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