Merge pull request #118 from donetick/capacitor-nfc-tag-scanner

add NFC capabilities for native apps
This commit is contained in:
Mohamad Tarbin
2026-06-07 16:36:24 -04:00
committed by GitHub
16 changed files with 381 additions and 124 deletions

View File

@@ -7,8 +7,8 @@ android {
applicationId "com.donetick.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 24
versionName "1.2.2"
versionCode 25
versionName "1.2.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

View File

@@ -18,6 +18,7 @@ dependencies {
implementation project(':capacitor-preferences')
implementation project(':capacitor-push-notifications')
implementation project(':capacitor-status-bar')
implementation project(':capgo-capacitor-nfc')
implementation project(':capgo-capacitor-social-login')
implementation project(':revenuecat-purchases-capacitor')
implementation project(':revenuecat-purchases-capacitor-ui')

View File

@@ -20,13 +20,20 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Deep link intent filter for OAuth callback -->
<!-- Deep link intent filter for OAuth and other donetick:// URLs -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="donetick" />
</intent-filter>
<!-- NFC NDEF dispatch: open app directly when a donetick:// tag is scanned -->
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="donetick" />
</intent-filter>
</activity>
<provider
@@ -56,4 +63,5 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.NFC" />
</manifest>

View File

@@ -29,6 +29,9 @@ project(':capacitor-push-notifications').projectDir = new File('../node_modules/
include ':capacitor-status-bar'
project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android')
include ':capgo-capacitor-nfc'
project(':capgo-capacitor-nfc').projectDir = new File('../node_modules/@capgo/capacitor-nfc/android')
include ':capgo-capacitor-social-login'
project(':capgo-capacitor-social-login').projectDir = new File('../node_modules/@capgo/capacitor-social-login/android')

View File

@@ -359,13 +359,13 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 24;
CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_TEAM = 6UJJ78R3BS;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.donetick.app;
MARKETING_VERSION = 1.2.2;
MARKETING_VERSION = 1.2.3;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -379,12 +379,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 24;
CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_TEAM = 6UJJ78R3BS;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.2.2;
MARKETING_VERSION = 1.2.3;
PRODUCT_BUNDLE_IDENTIFIER = com.donetick.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";

View File

@@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
<string>NDEF</string>
</array>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.applesignin</key>

View File

@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NFCReaderUsageDescription</key>
<string>Donetick uses NFC to read and write chore tags for quick task completion.</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>

View File

@@ -20,6 +20,7 @@ def capacitor_pods
pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences'
pod 'CapacitorPushNotifications', :path => '../../node_modules/@capacitor/push-notifications'
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
pod 'CapgoCapacitorNfc', :path => '../../node_modules/@capgo/capacitor-nfc'
pod 'CapgoCapacitorSocialLogin', :path => '../../node_modules/@capgo/capacitor-social-login'
pod 'RevenuecatPurchasesCapacitor', :path => '../../node_modules/@revenuecat/purchases-capacitor'
pod 'RevenuecatPurchasesCapacitorUi', :path => '../../node_modules/@revenuecat/purchases-capacitor-ui'

View File

@@ -35,6 +35,8 @@ PODS:
- Capacitor
- CapacitorStatusBar (7.0.2):
- Capacitor
- CapgoCapacitorNfc (7.2.0):
- Capacitor
- CapgoCapacitorSocialLogin (7.11.2):
- Alamofire (~> 5.10.2)
- Capacitor
@@ -145,6 +147,7 @@ DEPENDENCIES:
- "CapacitorPreferences (from `../../node_modules/@capacitor/preferences`)"
- "CapacitorPushNotifications (from `../../node_modules/@capacitor/push-notifications`)"
- "CapacitorStatusBar (from `../../node_modules/@capacitor/status-bar`)"
- "CapgoCapacitorNfc (from `../../node_modules/@capgo/capacitor-nfc`)"
- "CapgoCapacitorSocialLogin (from `../../node_modules/@capgo/capacitor-social-login`)"
- FirebaseMessaging
- "RevenuecatPurchasesCapacitor (from `../../node_modules/@revenuecat/purchases-capacitor`)"
@@ -202,6 +205,8 @@ EXTERNAL SOURCES:
:path: "../../node_modules/@capacitor/push-notifications"
CapacitorStatusBar:
:path: "../../node_modules/@capacitor/status-bar"
CapgoCapacitorNfc:
:path: "../../node_modules/@capgo/capacitor-nfc"
CapgoCapacitorSocialLogin:
:path: "../../node_modules/@capgo/capacitor-social-login"
RevenuecatPurchasesCapacitor:
@@ -225,6 +230,7 @@ SPEC CHECKSUMS:
CapacitorPreferences: 65107ed7437d96ee72583df5763985e3c0ff2bc2
CapacitorPushNotifications: 7c0659b349b149ee3936a682da31a9d269de9582
CapacitorStatusBar: e04d05e121d5a5979c29eb4249186a4e4a84cacb
CapgoCapacitorNfc: 3306447e38359522e6c1ea233aa6e84a54632534
CapgoCapacitorSocialLogin: a320106e1d032da88576cf8b31a336e971f5acad
FBAEMKit: e34530df538b8eb8aeb53c35867715ba6c63ef0c
FBSDKCoreKit: d3f479a69127acebb1c6aad91c1a33907bcf6c2f
@@ -250,6 +256,6 @@ SPEC CHECKSUMS:
SQLCipher: eb79c64049cb002b4e9fcb30edb7979bf4706dfc
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
PODFILE CHECKSUM: bb6dcef70c8edc058fe3ba311f08a223fd7dbc66
PODFILE CHECKSUM: 17f02615fe2b1c9437b2cd884eec64bc1229846c
COCOAPODS: 1.16.2

23
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "donetick",
"version": "1.2.1",
"version": "1.2.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "donetick",
"version": "1.2.1",
"version": "1.2.2",
"dependencies": {
"@capacitor-community/sqlite": "^7.0.0",
"@capacitor/android": "^7.0.0",
@@ -20,6 +20,7 @@
"@capacitor/preferences": "^7.0.0",
"@capacitor/push-notifications": "^7.0.0",
"@capacitor/status-bar": "^7.0.0",
"@capgo/capacitor-nfc": "^7.2.0",
"@capgo/capacitor-social-login": "^7.5.3",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
@@ -45,7 +46,6 @@
"esm": "^3.2.25",
"event-source-polyfill": "^1.0.31",
"fuse.js": "^7.0.0",
"i": "^0.3.7",
"i18next": "^25.8.14",
"i18next-browser-languagedetector": "^8.2.1",
"i18next-http-backend": "^3.0.2",
@@ -1947,6 +1947,15 @@
"@capacitor/core": ">=7.0.0"
}
},
"node_modules/@capgo/capacitor-nfc": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@capgo/capacitor-nfc/-/capacitor-nfc-7.2.0.tgz",
"integrity": "sha512-lYMlogAlUG3j1HmieOdFan4SNOnTbTvZ7BMYQmyhnfU9810aip9oPh6CYVXO/2ecZXW03ATTmSKtr4Ci8DpW2Q==",
"license": "MPL-2.0",
"peerDependencies": {
"@capacitor/core": ">=7.0.0"
}
},
"node_modules/@capgo/capacitor-social-login": {
"version": "7.11.2",
"license": "MIT",
@@ -8748,14 +8757,6 @@
"node": ">=4"
}
},
"node_modules/i": {
"version": "0.3.7",
"resolved": "https://registry.npmjs.org/i/-/i-0.3.7.tgz",
"integrity": "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q==",
"engines": {
"node": ">=0.4"
}
},
"node_modules/i18next": {
"version": "25.8.14",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.14.tgz",

View File

@@ -1,7 +1,7 @@
{
"name": "donetick",
"private": true,
"version": "1.2.2",
"version": "1.2.3",
"type": "module",
"engines": {
"node": ">=20.0.0",
@@ -33,7 +33,8 @@
"cap": "npm run build && npx cap sync",
"bump": "node bump-version.js",
"bump:minor": "node bump-version.js minor",
"bump:major": "node bump-version.js major"
"bump:major": "node bump-version.js major",
"bump:patch": "node bump-version.js patch"
},
"dependencies": {
"@capacitor-community/sqlite": "^7.0.0",
@@ -48,6 +49,7 @@
"@capacitor/preferences": "^7.0.0",
"@capacitor/push-notifications": "^7.0.0",
"@capacitor/status-bar": "^7.0.0",
"@capgo/capacitor-nfc": "^7.2.0",
"@capgo/capacitor-social-login": "^7.5.3",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",

View File

@@ -8,6 +8,35 @@ import { PushNotifications } from '@capacitor/push-notifications'
import { focusManager } from '@tanstack/react-query'
import { RegisterDeviceToken } from './utils/Fetcher'
// NFC chore deep link: donetick://chores/123?auto_complete=true
const handleNFCChoreDeepLink = url => {
try {
const urlObj = new URL(url)
// donetick://chores/123 → host='chores', pathname='/123'
const choreId = urlObj.pathname.slice(1)
const autoComplete = urlObj.searchParams.get('auto_complete')
const path = `/chores/${choreId}${autoComplete ? '?auto_complete=' + autoComplete : ''}`
// getLaunchUrl() persists across every WebView reload caused by window.location.href.
// If we're already on the target page, skip to avoid an infinite reload loop.
if (window.location.pathname + window.location.search === path) return
console.log('[NFC] navigating to', path)
window.location.href = path
} catch (error) {
console.error('[NFC] Error handling chore deep link:', error)
}
}
const handleUrlOpen = url => {
console.log('[NFC] handleUrlOpen:', url)
if (url.startsWith('donetick://chores/')) {
handleNFCChoreDeepLink(url)
} else if (url.startsWith('donetick://auth/')) {
handleOAuthDeepLink(url)
}
}
// OAuth callback handler for deep links
const handleOAuthDeepLink = async url => {
console.log('OAuth deep link received:', url)
@@ -215,16 +244,20 @@ const registerCapacitorListeners = () => {
return
}
localNotificationListenerRegistration()
// Register deep link handler for OAuth and other deep links
mobileApp.addListener('appUrlOpen', event => {
console.log('App URL opened:', event.url)
// Handle OAuth callback
if (event.url.startsWith('donetick://auth/')) {
handleOAuthDeepLink(event.url)
// Cold-start: app was launched by tapping an NFC tag (or other deep link)
mobileApp.getLaunchUrl().then(result => {
if (result?.url) {
console.log('[NFC] getLaunchUrl:', result.url)
handleUrlOpen(result.url)
}
})
// Foreground / singleTask resume: app was already running when the tag was tapped
mobileApp.addListener('appUrlOpen', event => {
console.log('[NFC] appUrlOpen:', event.url)
handleUrlOpen(event.url)
})
mobileApp.addListener('appStateChange', ({ isActive }) => {
focusManager.setFocused(isActive)
@@ -233,6 +266,9 @@ const registerCapacitorListeners = () => {
mobileApp.addListener('backButton', ({ canGoBack }) => {
if (canGoBack) {
window.history.back()
} else if (window.location.pathname !== '/') {
// No history (e.g. app launched directly to a chore via NFC) — go home
window.location.href = '/'
} else {
mobileApp.exitApp()
}

View File

@@ -1,11 +1,136 @@
import { CapacitorNfc } from '@capgo/capacitor-nfc'
// Encodes a URL into an NDEF URI record (TNF=0x01, type='U')
const buildUriRecord = url => {
const encoder = new TextEncoder()
let prefixByte = 0x00
let uriStr = url
if (url.startsWith('https://')) {
prefixByte = 0x04
uriStr = url.slice(8)
} else if (url.startsWith('http://')) {
prefixByte = 0x03
uriStr = url.slice(7)
}
return {
tnf: 0x01,
type: [0x55],
id: [],
payload: [prefixByte, ...Array.from(encoder.encode(uriStr))],
}
}
// Decodes a URL from an NDEF URI record payload. Returns null if not a URI record.
export const decodeNdefUrl = record => {
if (!record || record.tnf !== 0x01) return null
if (record.type.length !== 1 || record.type[0] !== 0x55) return null
const payload = record.payload
if (!payload || payload.length === 0) return null
const prefixes = [
'',
'http://www.',
'https://www.',
'http://',
'https://',
'tel:',
'mailto:',
]
const prefix = prefixes[payload[0]] ?? ''
const uri = new TextDecoder().decode(new Uint8Array(payload.slice(1)))
return prefix + uri
}
// Starts a native NFC write session. Calls onWaiting once scanning is active,
// then onSuccess or onError when the write completes. Returns a cancel function.
export const startNativeNFCWrite = async (url, { onWaiting, onSuccess, onError }) => {
let listener = null
let done = false
const cleanup = async () => {
if (listener) {
await listener.remove()
listener = null
}
await CapacitorNfc.stopScanning().catch(() => {})
}
try {
listener = await CapacitorNfc.addListener('nfcEvent', async () => {
if (done) return
done = true
try {
await CapacitorNfc.write({ records: [buildUriRecord(url)] })
await cleanup()
onSuccess()
} catch (err) {
await cleanup()
onError(err.message || 'Failed to write to NFC tag')
}
})
await CapacitorNfc.startScanning({
alertMessage: 'Hold your device near the NFC tag to write',
invalidateAfterFirstRead: true,
// Without FLAG_READER_SKIP_NDEF_CHECK (0x80), Android enumerates
// Ndef/NdefFormatable tech so the plugin can format blank tags on write.
androidReaderModeFlags: 0x0f, // NFC_A | NFC_B | NFC_F | NFC_V
})
onWaiting()
return cleanup
} catch (err) {
await cleanup()
onError(err.message || 'Failed to start NFC session')
return async () => {}
}
}
// Starts a native NFC scan session for reading. Calls onTag(url) when a URL
// NDEF record is found, or onError on failure. Returns a cancel function.
export const startNativeScan = async ({ onTag, onError }) => {
let listener = null
let done = false
const cleanup = async () => {
if (listener) {
await listener.remove()
listener = null
}
await CapacitorNfc.stopScanning().catch(() => {})
}
try {
listener = await CapacitorNfc.addListener('nfcEvent', async event => {
if (done) return
const records = event.tag?.ndefMessage ?? []
for (const record of records) {
const url = decodeNdefUrl(record)
if (url) {
done = true
await cleanup()
onTag(url)
return
}
}
})
await CapacitorNfc.startScanning({
alertMessage: 'Hold your device near the NFC tag',
invalidateAfterFirstRead: true,
})
return cleanup
} catch (err) {
await cleanup()
onError(err.message || 'Failed to start NFC session')
return async () => {}
}
}
// Legacy default export for web/PWA (NDEFReader API)
const writeToNFC = async url => {
if ('NDEFReader' in window) {
try {
const ndef = new window.NDEFReader()
await ndef.write({
records: [{ recordType: 'url', data: url }],
})
alert('URL written to NFC tag successfully!')
await ndef.write({ records: [{ recordType: 'url', data: url }] })
} catch (error) {
console.error('Error writing to NFC tag:', error)
alert('Error writing to NFC tag. Please try again.')

View File

@@ -158,8 +158,8 @@ const ChoreView = () => {
document.title = 'Donetick: ' + choreData.res.name
setPerformers(circleMembersData.res)
const auto_complete = searchParams.get('auto_complete')
if (auto_complete === 'true') {
if (searchParams.get('auto_complete') === 'true') {
navigate({ search: '' }, { replace: true })
handleTaskCompletion()
}
}, [choreData, circleMembersData])

View File

@@ -1,9 +1,15 @@
import { Capacitor } from '@capacitor/core'
import DateModal from '../../Modals/Inputs/DateModal'
import NudgeModal from '../../Modals/Inputs/NudgeModal'
import SelectModal from '../../Modals/Inputs/SelectModal'
import TextModal from '../../Modals/Inputs/TextModal'
import WriteNFCModal from '../../Modals/Inputs/WriteNFCModal'
const getNFCUrl = choreId =>
Capacitor.getPlatform() === 'android'
? `donetick://chores/${choreId}`
: `${window.location.origin}/chores/${choreId}`
const ChoreModals = ({
activeModal,
modalChore,
@@ -65,12 +71,13 @@ const ChoreModals = ({
<WriteNFCModal
config={{
isOpen: true,
url: `${window.location.origin}/chores/${modalChore.id}`,
url: getNFCUrl(modalChore.id),
onClose: onClose,
}}
/>
)}
{activeModal === 'nudge' && modalChore && (
<NudgeModal
config={{

View File

@@ -1,114 +1,175 @@
import { CopyAll } from '@mui/icons-material'
import { Box, Button, Checkbox, Input, ListItem, Typography } from '@mui/joy'
import { useState } from 'react'
import {
Box,
Button,
Checkbox,
CircularProgress,
Input,
ListItem,
Typography,
} from '@mui/joy'
import { useRef, useState } from 'react'
import { Capacitor } from '@capacitor/core'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
import { startNativeNFCWrite } from '../../../service/NFCWriter'
function WriteNFCModal({ config }) {
const { ResponsiveModal } = useResponsiveModal()
const [nfcStatus, setNfcStatus] = useState('idle') // 'idle', 'writing', 'success', 'error'
const [nfcStatus, setNfcStatus] = useState('idle') // 'idle' | 'writing' | 'waiting_for_tag' | 'success' | 'error'
const [errorMessage, setErrorMessage] = useState('')
const [isAutoCompleteWhenScan, setIsAutoCompleteWhenScan] = useState(false)
const cancelScanRef = useRef(null)
const isNative = Capacitor.isNativePlatform()
const requestNFCAccess = async () => {
if ('NDEFReader' in window) {
// Assuming permission request is implicit in 'write' or 'scan' methods
setNfcStatus('idle')
} else {
alert('NFC is not supported by this browser.')
}
const getURL = () => {
let url = config.url
if (isAutoCompleteWhenScan) url += '?auto_complete=true'
return url
}
const writeToNFC = async url => {
if ('NDEFReader' in window) {
try {
const ndef = new window.NDEFReader()
await ndef.write({
records: [{ recordType: 'url', data: url }],
})
setNfcStatus('success')
} catch (error) {
console.error('Error writing to NFC tag:', error)
setNfcStatus('error')
setErrorMessage('Error writing to NFC tag. Please try again.')
}
} else {
setNfcStatus('error')
setErrorMessage(
'NFC is not supported by this browser. You can still copy the URL and write it to an NFC tag using a compatible device.',
)
const handleClose = async () => {
if (cancelScanRef.current) {
await cancelScanRef.current()
cancelScanRef.current = null
}
}
const handleClose = () => {
config.onClose()
setNfcStatus('idle')
setErrorMessage('')
}
const getURL = () => {
let url = config.url
if (isAutoCompleteWhenScan) {
url = url + '?auto_complete=true'
const handleCancel = async () => {
if (cancelScanRef.current) {
await cancelScanRef.current()
cancelScanRef.current = null
}
setNfcStatus('idle')
}
const writeToNFC = async () => {
const url = getURL()
if (isNative) {
setNfcStatus('writing')
const cancel = await startNativeNFCWrite(url, {
onWaiting: () => setNfcStatus('waiting_for_tag'),
onSuccess: () => {
cancelScanRef.current = null
setNfcStatus('success')
},
onError: msg => {
cancelScanRef.current = null
setNfcStatus('error')
setErrorMessage(msg)
},
})
cancelScanRef.current = cancel
} else {
if ('NDEFReader' in window) {
try {
setNfcStatus('writing')
const ndef = new window.NDEFReader()
await ndef.write({ records: [{ recordType: 'url', data: url }] })
setNfcStatus('success')
} catch (error) {
console.error('Error writing to NFC tag:', error)
setNfcStatus('error')
setErrorMessage('Error writing to NFC tag. Please try again.')
}
} else {
setNfcStatus('error')
setErrorMessage(
'NFC is not supported by this browser. You can still copy the URL and write it to an NFC tag using a compatible device.',
)
}
}
}
const renderBody = () => {
if (nfcStatus === 'success') {
return (
<Typography level='body-md' gutterBottom>
URL written to NFC tag successfully!
</Typography>
)
}
return url
if (nfcStatus === 'waiting_for_tag') {
return (
<>
<Box
display='flex'
flexDirection='column'
alignItems='center'
gap={2}
py={3}
>
<CircularProgress size='lg' />
<Typography level='body-md' textAlign='center'>
Hold your device near the NFC tag
</Typography>
</Box>
<Button
variant='outlined'
color='neutral'
fullWidth
onClick={handleCancel}
>
Cancel
</Button>
</>
)
}
return (
<>
<Typography level='body-md' gutterBottom>
{nfcStatus === 'error'
? errorMessage
: 'Press the button below to write to NFC.'}
</Typography>
<Input
value={getURL()}
fullWidth
readOnly
label='URL'
sx={{ mt: 1 }}
endDecorator={
<CopyAll
sx={{ cursor: 'pointer' }}
onClick={() => {
navigator.clipboard.writeText(getURL())
alert('URL copied to clipboard!')
}}
/>
}
/>
<ListItem>
<Checkbox
checked={isAutoCompleteWhenScan}
onChange={e => setIsAutoCompleteWhenScan(e.target.checked)}
label='Auto-complete when scanned'
/>
</ListItem>
<Box display='flex' justifyContent='space-around' mt={1}>
<Button
size='lg'
onClick={writeToNFC}
fullWidth
disabled={nfcStatus === 'writing'}
>
Write NFC
</Button>
</Box>
</>
)
}
return (
<ResponsiveModal open={config?.isOpen} onClose={handleClose}>
<Typography level='h4' mb={1}>
{nfcStatus === 'success' ? 'Success!' : 'Write to NFC'}
</Typography>
{nfcStatus === 'success' ? (
<Typography level='body-md' gutterBottom>
URL written to NFC tag successfully!
</Typography>
) : (
<>
<Typography level='body-md' gutterBottom>
{nfcStatus === 'error'
? errorMessage
: 'Press the button below to write to NFC.'}
</Typography>
<Input
value={getURL()}
fullWidth
readOnly
label='URL'
sx={{ mt: 1 }}
endDecorator={
<CopyAll
sx={{ cursor: 'pointer' }}
onClick={() => {
navigator.clipboard.writeText(getURL())
alert('URL copied to clipboard!')
}}
/>
}
/>
<ListItem>
<Checkbox
checked={isAutoCompleteWhenScan}
onChange={e => setIsAutoCompleteWhenScan(e.target.checked)}
label='Auto-complete when scanned'
/>
</ListItem>
<Box display={'flex'} justifyContent={'space-around'} mt={1}>
<Button
size='lg'
onClick={() => writeToNFC(getURL())}
fullWidth
sx={{ mr: 1 }}
disabled={nfcStatus === 'writing'}
>
Write NFC
</Button>
<Button size='lg' onClick={requestNFCAccess} variant='outlined'>
Request Access
</Button>
</Box>
</>
)}
{renderBody()}
</ResponsiveModal>
)
}