Merge pull request #118 from donetick/capacitor-nfc-tag-scanner
add NFC capabilities for native apps
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
23
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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.')
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user