From 48adb0b256fd0ad00238193c82e076c2d5f5433b Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sun, 7 Jun 2026 16:32:43 -0400 Subject: [PATCH] feat: update app version to 1.2.3, add NFC capabilities, and enhance deep link handling --- android/app/build.gradle | 4 +- android/app/capacitor.build.gradle | 1 + android/app/src/main/AndroidManifest.xml | 10 +- android/capacitor.settings.gradle | 3 + ios/App/App.xcodeproj/project.pbxproj | 8 +- ios/App/App/App.entitlements | 4 + ios/App/App/Info.plist | 2 + ios/App/Podfile | 1 + ios/App/Podfile.lock | 8 +- package-lock.json | 23 +- package.json | 6 +- src/CapacitorListener.js | 52 ++++- src/service/NFCWriter.jsx | 133 ++++++++++- src/views/ChoreEdit/ChoreView.jsx | 4 +- src/views/Chores/components/ChoreModals.jsx | 9 +- src/views/Modals/Inputs/WriteNFCModal.jsx | 237 ++++++++++++-------- 16 files changed, 381 insertions(+), 124 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index d827280..b90cbe7 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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. diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index 1e82591..9f6e577 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -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') diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 3d5b3c5..76aa59f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -20,13 +20,20 @@ - + + + + + + + + + diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index 3ad1593..e49f972 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -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') diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index be59e39..02f8c99 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -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 = ""; diff --git a/ios/App/App/App.entitlements b/ios/App/App/App.entitlements index 80b5221..d2bef04 100644 --- a/ios/App/App/App.entitlements +++ b/ios/App/App/App.entitlements @@ -2,6 +2,10 @@ + com.apple.developer.nfc.readersession.formats + + NDEF + aps-environment development com.apple.developer.applesignin diff --git a/ios/App/App/Info.plist b/ios/App/App/Info.plist index 533cd70..5a58712 100644 --- a/ios/App/App/Info.plist +++ b/ios/App/App/Info.plist @@ -2,6 +2,8 @@ + NFCReaderUsageDescription + Donetick uses NFC to read and write chore tags for quick task completion. CFBundleDevelopmentRegion en CFBundleDisplayName diff --git a/ios/App/Podfile b/ios/App/Podfile index 9602381..e5d54c4 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -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' diff --git a/ios/App/Podfile.lock b/ios/App/Podfile.lock index c8fb8cb..2fef6fb 100644 --- a/ios/App/Podfile.lock +++ b/ios/App/Podfile.lock @@ -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 diff --git a/package-lock.json b/package-lock.json index cd71ed0..c02d640 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 63a5185..6aed4fc 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/CapacitorListener.js b/src/CapacitorListener.js index 32bd343..c8fdf21 100644 --- a/src/CapacitorListener.js +++ b/src/CapacitorListener.js @@ -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() } diff --git a/src/service/NFCWriter.jsx b/src/service/NFCWriter.jsx index 91ac978..cc6f37a 100644 --- a/src/service/NFCWriter.jsx +++ b/src/service/NFCWriter.jsx @@ -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.') diff --git a/src/views/ChoreEdit/ChoreView.jsx b/src/views/ChoreEdit/ChoreView.jsx index 9548bc8..1123486 100644 --- a/src/views/ChoreEdit/ChoreView.jsx +++ b/src/views/ChoreEdit/ChoreView.jsx @@ -120,8 +120,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]) diff --git a/src/views/Chores/components/ChoreModals.jsx b/src/views/Chores/components/ChoreModals.jsx index 2a2a6e8..9191088 100644 --- a/src/views/Chores/components/ChoreModals.jsx +++ b/src/views/Chores/components/ChoreModals.jsx @@ -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 = ({ )} + {activeModal === 'nudge' && modalChore && ( { - 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 ( + + URL written to NFC tag successfully! + + ) } - return url + if (nfcStatus === 'waiting_for_tag') { + return ( + <> + + + + Hold your device near the NFC tag + + + + + ) + } + + return ( + <> + + {nfcStatus === 'error' + ? errorMessage + : 'Press the button below to write to NFC.'} + + { + navigator.clipboard.writeText(getURL()) + alert('URL copied to clipboard!') + }} + /> + } + /> + + setIsAutoCompleteWhenScan(e.target.checked)} + label='Auto-complete when scanned' + /> + + + + + + ) } + return ( {nfcStatus === 'success' ? 'Success!' : 'Write to NFC'} - - {nfcStatus === 'success' ? ( - - URL written to NFC tag successfully! - - ) : ( - <> - - {nfcStatus === 'error' - ? errorMessage - : 'Press the button below to write to NFC.'} - - { - navigator.clipboard.writeText(getURL()) - alert('URL copied to clipboard!') - }} - /> - } - /> - - setIsAutoCompleteWhenScan(e.target.checked)} - label='Auto-complete when scanned' - /> - - - - - - - )} + {renderBody()} ) }