diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index bd006b4d..00000000 --- a/.eslintrc.js +++ /dev/null @@ -1,32 +0,0 @@ -module.exports = { - root: true, - env: { - node: true, - es2022: true, - }, - extends: [ - "plugin:vue/vue3-recommended", - "eslint:recommended", - "@vue/typescript/recommended", - "plugin:prettier/recommended" - ], - // parserOptions: { - // ecmaVersion: 2020, - // }, - rules: { - "max-len": ["warn", { - code: 100, - ignoreComments: true, - ignorePattern: '^\\s*class="[^"]*"$', - ignoreStrings: true, - ignoreTemplateLiterals: true, - ignoreUrls: true, - }], - "no-console": process.env.NODE_ENV === "production" ? "error" : "warn", - "no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn", - "@typescript-eslint/no-explicit-any": "warn", - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/no-unnecessary-type-constraint": "off", - "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }] - }, -}; diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..50c90113 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,57 @@ +{ + "root": true, + "env": { + "node": true, + "browser": true, + "es2022": true + }, + "extends": [ + "plugin:vue/vue3-recommended", + "eslint:recommended", + "@vue/typescript/recommended", + "plugin:prettier/recommended" + ], + "parser": "vue-eslint-parser", + "parserOptions": { + "parser": "@typescript-eslint/parser", + "ecmaVersion": 2022, + "sourceType": "module", + "extraFileExtensions": [".vue"], + "ecmaFeatures": { + "jsx": true + } + }, + "plugins": [ + "@typescript-eslint", + "vue", + "prettier" + ], + "rules": { + "no-console": "warn", + "no-debugger": "warn", + "@typescript-eslint/no-explicit-any": "off", + "vue/multi-word-component-names": "off", + "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], + "@typescript-eslint/no-unnecessary-type-constraint": "off", + "vue/no-parsing-error": ["error", { + "x-invalid-end-tag": false, + "invalid-first-character-of-tag-name": false + }], + "vue/no-v-html": "warn", + "prettier/prettier": ["error", { + "singleQuote": true, + "semi": false, + "trailingComma": "none" + }] + }, + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.mts"], + "parser": "@typescript-eslint/parser" + }, + { + "files": ["*.js", "*.jsx", "*.mjs"], + "parser": "@typescript-eslint/parser" + } + ] +} \ No newline at end of file diff --git a/.eslintrc.mjs b/.eslintrc.mjs new file mode 100644 index 00000000..a317c7b7 --- /dev/null +++ b/.eslintrc.mjs @@ -0,0 +1,55 @@ +export default { + root: true, + env: { + node: true, + browser: true, + es2022: true + }, + extends: [ + 'plugin:vue/vue3-recommended', + 'eslint:recommended', + '@vue/typescript/recommended' + ], + parser: 'vue-eslint-parser', + parserOptions: { + parser: { + 'ts': '@typescript-eslint/parser', + 'js': '@typescript-eslint/parser', + ' diff --git a/src/components/ChoiceButtonDialog.vue b/src/components/ChoiceButtonDialog.vue index 241a1b73..db074d8f 100644 --- a/src/components/ChoiceButtonDialog.vue +++ b/src/components/ChoiceButtonDialog.vue @@ -26,7 +26,9 @@ >
{{ title }} -

{{ text }}

+

+ {{ text }} +

@@ -71,92 +71,92 @@ diff --git a/src/components/ImageMethodDialog.vue b/src/components/ImageMethodDialog.vue index 8a3faaa2..f7a27bd9 100644 --- a/src/components/ImageMethodDialog.vue +++ b/src/components/ImageMethodDialog.vue @@ -12,7 +12,7 @@ class="text-lg text-center px-2 py-0.5 leading-none absolute right-0 top-0 text-white" @click="close()" > - + @@ -56,103 +56,102 @@ diff --git a/src/components/ImageViewer.vue b/src/components/ImageViewer.vue index 78eb818e..89ba029e 100644 --- a/src/components/ImageViewer.vue +++ b/src/components/ImageViewer.vue @@ -38,44 +38,44 @@ diff --git a/src/components/InviteDialog.vue b/src/components/InviteDialog.vue index 73d95e27..febdd9ea 100644 --- a/src/components/InviteDialog.vue +++ b/src/components/InviteDialog.vue @@ -46,50 +46,50 @@ diff --git a/src/components/MembersList.vue b/src/components/MembersList.vue index a61b0c83..71af5f2b 100644 --- a/src/components/MembersList.vue +++ b/src/components/MembersList.vue @@ -80,7 +80,9 @@ >
-

{{ member.name }}

+

+ {{ member.name }} +

@@ -157,138 +159,138 @@ diff --git a/src/components/OnboardingDialog.vue b/src/components/OnboardingDialog.vue index 85c6eb9f..804e8a4e 100644 --- a/src/components/OnboardingDialog.vue +++ b/src/components/OnboardingDialog.vue @@ -198,64 +198,64 @@ diff --git a/src/components/QRScanner/CapacitorScanner.ts b/src/components/QRScanner/CapacitorScanner.ts new file mode 100644 index 00000000..26abe667 --- /dev/null +++ b/src/components/QRScanner/CapacitorScanner.ts @@ -0,0 +1,124 @@ +import { + BarcodeScanner, + BarcodeFormat, + LensFacing, + ScanResult +} from '@capacitor-mlkit/barcode-scanning' +import type { QRScannerService, ScanListener } from './types' +import { logger } from '../../utils/logger' + +export class CapacitorQRScanner implements QRScannerService { + private scanListener: ScanListener | null = null + private isScanning = false + private listenerHandles: Array<() => Promise> = [] + + async checkPermissions() { + try { + const { camera } = await BarcodeScanner.checkPermissions() + return camera === 'granted' + } catch (error) { + logger.error('Error checking camera permissions:', error) + return false + } + } + + async requestPermissions() { + try { + const { camera } = await BarcodeScanner.requestPermissions() + return camera === 'granted' + } catch (error) { + logger.error('Error requesting camera permissions:', error) + return false + } + } + + async isSupported() { + try { + const { supported } = await BarcodeScanner.isSupported() + return supported + } catch (error) { + logger.error('Error checking barcode scanner support:', error) + return false + } + } + + async startScan() { + if (this.isScanning) { + logger.warn('Scanner is already active') + return + } + + try { + // First register listeners before starting scan + await this.registerListeners() + + this.isScanning = true + await BarcodeScanner.startScan({ + formats: [BarcodeFormat.QrCode], + lensFacing: LensFacing.Back + }) + } catch (error) { + // Ensure cleanup on error + this.isScanning = false + await this.removeListeners() + logger.error('Error starting barcode scan:', error) + throw error + } + } + + private async registerListeners() { + try { + const handle = await BarcodeScanner.addListener( + 'barcodesScanned', + async (result: ScanResult) => { + if (result.barcodes.length > 0 && this.scanListener) { + const barcode = result.barcodes[0] + this.scanListener.onScan(barcode.rawValue) + await this.stopScan() + } + } + ) + this.listenerHandles.push(() => handle.remove()) + } catch (error) { + logger.error('Error registering barcode listener:', error) + throw error + } + } + + private async removeListeners() { + for (const remove of this.listenerHandles) { + try { + await remove() + } catch (error) { + logger.error('Error removing listener:', error) + } + } + this.listenerHandles = [] + } + + async stopScan() { + if (!this.isScanning) { + return + } + + try { + // First stop the scan + await BarcodeScanner.stopScan() + } catch (error) { + logger.error('Error stopping barcode scan:', error) + } finally { + // Always cleanup state even if stop fails + this.isScanning = false + await this.removeListeners() + } + } + + addListener(listener: ScanListener): void { + this.scanListener = listener + } + + async cleanup(): Promise { + await this.stopScan() + this.scanListener = null + } +} diff --git a/src/components/QRScannerDialog.vue b/src/components/QRScanner/QRScannerDialog.vue similarity index 56% rename from src/components/QRScannerDialog.vue rename to src/components/QRScanner/QRScannerDialog.vue index a14fe48f..23c05827 100644 --- a/src/components/QRScannerDialog.vue +++ b/src/components/QRScanner/QRScannerDialog.vue @@ -38,7 +38,9 @@
-

{{ state.processingDetails }}

+

+ {{ state.processingDetails }} +

@@ -55,216 +57,216 @@ diff --git a/src/components/QRScanner/WebDialogQRScanner.ts b/src/components/QRScanner/WebDialogQRScanner.ts new file mode 100644 index 00000000..f1f41f40 --- /dev/null +++ b/src/components/QRScanner/WebDialogQRScanner.ts @@ -0,0 +1,74 @@ +import type { QRScannerService, ScanListener } from './types' +import QRScannerDialog from './QRScannerDialog.vue' +import { createApp, type App } from 'vue' +import { logger } from '../../utils/logger' + +// Import platform-specific flags from Vite config +declare const __USE_QR_READER__: boolean + +export class WebDialogQRScanner implements QRScannerService { + private dialogApp: App | null = null + private dialogElement: HTMLDivElement | null = null + private scanListener: ScanListener | null = null + + async checkPermissions(): Promise { + return navigator?.mediaDevices !== undefined + } + + async requestPermissions(): Promise { + try { + const stream = await navigator.mediaDevices.getUserMedia({ video: true }) + stream.getTracks().forEach((track) => track.stop()) + return true + } catch (error) { + logger.error('Failed to get camera permissions:', error) + return false + } + } + + async isSupported(): Promise { + return Promise.resolve( + __USE_QR_READER__ && navigator?.mediaDevices !== undefined + ) + } + + async startScan(): Promise { + if (!(await this.isSupported())) { + throw new Error('QR scanning is not supported in this environment') + } + + this.dialogElement = document.createElement('div') + document.body.appendChild(this.dialogElement) + + this.dialogApp = createApp(QRScannerDialog, { + onScan: (result: string) => { + if (this.scanListener) { + this.scanListener.onScan(result) + } + }, + onClose: () => { + this.stopScan() + } + }) + + this.dialogApp.mount(this.dialogElement) + } + + async stopScan(): Promise { + if (this.dialogApp && this.dialogElement) { + this.dialogApp.unmount() + this.dialogElement.remove() + this.dialogApp = null + this.dialogElement = null + } + } + + addListener(listener: ScanListener): void { + this.scanListener = listener + } + + async cleanup(): Promise { + await this.stopScan() + this.scanListener = null + } +} diff --git a/src/components/QRScanner/factory.ts b/src/components/QRScanner/factory.ts new file mode 100644 index 00000000..3e85389c --- /dev/null +++ b/src/components/QRScanner/factory.ts @@ -0,0 +1,38 @@ +import { Capacitor } from '@capacitor/core' +import type { QRScannerService } from './types' +import { logger } from '../../utils/logger' +import { WebDialogQRScanner } from './WebDialogScanner' +import { CapacitorQRScanner } from './CapacitorScanner' + +// Import platform-specific flags from Vite config +declare const __USE_QR_READER__: boolean +declare const __IS_MOBILE__: boolean + +export class QRScannerFactory { + private static instance: QRScannerService | null = null + + static getInstance(): QRScannerService { + if (!this.instance) { + // Use platform-specific flags for more accurate detection + if (__IS_MOBILE__ || Capacitor.isNativePlatform()) { + logger.log('Creating native QR scanner instance') + this.instance = new CapacitorQRScanner() + } else if (__USE_QR_READER__) { + logger.log('Creating web QR scanner instance') + this.instance = new WebDialogQRScanner() + } else { + throw new Error( + 'No QR scanner implementation available for this platform' + ) + } + } + return this.instance + } + + static async cleanup() { + if (this.instance) { + await this.instance.cleanup() + this.instance = null + } + } +} diff --git a/src/components/QRScanner/types.ts b/src/components/QRScanner/types.ts new file mode 100644 index 00000000..70547ca9 --- /dev/null +++ b/src/components/QRScanner/types.ts @@ -0,0 +1,14 @@ +export interface ScanListener { + onScan: (result: string) => void + onError?: (error: Error) => void +} + +export interface QRScannerService { + checkPermissions(): Promise + requestPermissions(): Promise + isSupported(): Promise + startScan(): Promise + stopScan(): Promise + addListener(listener: ScanListener): void + cleanup(): Promise +} diff --git a/src/components/QuickNav.vue b/src/components/QuickNav.vue index 86b1c9d6..a6e451d3 100644 --- a/src/components/QuickNav.vue +++ b/src/components/QuickNav.vue @@ -8,7 +8,7 @@ 'basis-1/5': true, 'rounded-md': true, 'bg-slate-400 text-white': selected === 'Home', - 'text-slate-500': selected !== 'Home', + 'text-slate-500': selected !== 'Home' }" > @@ -24,7 +24,7 @@ 'basis-1/5': true, 'rounded-md': true, 'bg-slate-400 text-white': selected === 'Discover', - 'text-slate-500': selected !== 'Discover', + 'text-slate-500': selected !== 'Discover' }" > diff --git a/src/components/TopMessage.vue b/src/components/TopMessage.vue index 01f623d2..23ec3a8c 100644 --- a/src/components/TopMessage.vue +++ b/src/components/TopMessage.vue @@ -13,46 +13,46 @@ diff --git a/src/views/ConfirmGiftView.vue b/src/views/ConfirmGiftView.vue index 8a33cf92..ddc42849 100644 --- a/src/views/ConfirmGiftView.vue +++ b/src/views/ConfirmGiftView.vue @@ -19,7 +19,7 @@ isRegistered, veriClaim, activeDid, - confirmerIdList, + confirmerIdList ) " > @@ -37,7 +37,7 @@ isRegistered, veriClaim, activeDid, - confirmerIdList, + confirmerIdList ) " class="col-span-1 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md" @@ -81,7 +81,7 @@
- {{ giveDetails.amount ? "and:" : "" }} + {{ giveDetails.amount ? 'and:' : '' }} {{ giveDetails.description }}
to
@@ -134,7 +134,7 @@ This fulfills {{ capitalizeAndInsertSpacesBeforeCapsWithAPrefix( - giveDetails?.fulfillsType || "", + giveDetails?.fulfillsType || '' ) }} @@ -242,7 +242,7 @@ @click=" copyToClipboard( 'The DID of ' + confsVisibleTo, - confsVisibleTo, + confsVisibleTo ) " > @@ -382,7 +382,7 @@ class="text-blue-500" >{{ veriClaim.publicUrls[visDid].substring( - veriClaim.publicUrls[visDid].indexOf("//") + 2, + veriClaim.publicUrls[visDid].indexOf('//') + 2 ) }} @@ -426,28 +426,28 @@ v-if="isLoading" class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full" > - +
diff --git a/src/views/ContactAmountsView.vue b/src/views/ContactAmountsView.vue index d984d161..d201102d 100644 --- a/src/views/ContactAmountsView.vue +++ b/src/views/ContactAmountsView.vue @@ -1,5 +1,5 @@ diff --git a/src/views/ContactEditView.vue b/src/views/ContactEditView.vue index 34d7dd76..415ada1b 100644 --- a/src/views/ContactEditView.vue +++ b/src/views/ContactEditView.vue @@ -41,7 +41,7 @@ v-model="contactNotes" rows="4" class="block w-full mt-1 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" - > + /> @@ -132,15 +132,15 @@ diff --git a/src/views/ContactGiftingView.vue b/src/views/ContactGiftingView.vue index aa48f796..925cbafc 100644 --- a/src/views/ContactGiftingView.vue +++ b/src/views/ContactGiftingView.vue @@ -1,5 +1,5 @@ diff --git a/src/views/ContactImportView.vue b/src/views/ContactImportView.vue index 7e25e85e..dc0d729f 100644 --- a/src/views/ContactImportView.vue +++ b/src/views/ContactImportView.vue @@ -1,5 +1,5 @@ diff --git a/src/views/ContactScanView.vue b/src/views/ContactScanView.vue index 7e6ba7f8..631e1a9f 100644 --- a/src/views/ContactScanView.vue +++ b/src/views/ContactScanView.vue @@ -7,7 +7,8 @@ + > + Scan Contact @@ -20,31 +21,31 @@ -
+
-
+
-
+
-
+
+ />
+ />
+ />
+ />

…or Enter Contact Data

@@ -83,10 +84,10 @@ diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index 9081effb..83c6fa36 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -48,7 +48,7 @@ @click=" warning( 'You must get registered before you can create invites.', - 'Not Registered', + 'Not Registered' ) " /> @@ -62,7 +62,7 @@ @click=" warning( 'You must get registered before you can initiate an onboarding meeting.', - 'Not Registered', + 'Not Registered' ) " /> @@ -134,7 +134,7 @@ @click="toggleShowContactAmounts()" > {{ - showGiveNumbers ? "Hide Hours, Offer, etc" : "See Hours, Offer, etc" + showGiveNumbers ? 'Hide Hours, Offer, etc' : 'See Hours, Offer, etc' }}
@@ -157,10 +157,10 @@ > {{ showGiveTotals - ? "Totals" + ? 'Totals' : showGiveConfirmed - ? "Confirmed Amounts" - : "Unconfirmed Amounts" + ? 'Confirmed Amounts' + : 'Unconfirmed Amounts' }} @@ -191,7 +191,7 @@ contactsSelected.includes(contact.did) ? contactsSelected.splice( contactsSelected.indexOf(contact.did), - 1, + 1 ) : contactsSelected.push(contact.did) " @@ -212,7 +212,7 @@
@@ -246,11 +246,11 @@ /* eslint-disable prettier/prettier */ showGiveTotals ? ((givenToMeConfirmed[contact.did] || 0) - + (givenToMeUnconfirmed[contact.did] || 0)) + + (givenToMeUnconfirmed[contact.did] || 0)) : showGiveConfirmed - ? (givenToMeConfirmed[contact.did] || 0) - : (givenToMeUnconfirmed[contact.did] || 0) - /* eslint-enable prettier/prettier */ + ? (givenToMeConfirmed[contact.did] || 0) + : (givenToMeUnconfirmed[contact.did] || 0) + /* eslint-enable prettier/prettier */ }} @@ -267,9 +267,9 @@ ? ((givenByMeConfirmed[contact.did] || 0) + (givenByMeUnconfirmed[contact.did] || 0)) : showGiveConfirmed - ? (givenByMeConfirmed[contact.did] || 0) - : (givenByMeUnconfirmed[contact.did] || 0) - /* eslint-enable prettier/prettier */ + ? (givenByMeConfirmed[contact.did] || 0) + : (givenByMeUnconfirmed[contact.did] || 0) + /* eslint-enable prettier/prettier */ }} @@ -284,7 +284,7 @@