From 6007bc34e435b1f0db8fb13f074c8fbe5c812804 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Wed, 30 Jul 2025 12:47:55 +0000 Subject: [PATCH] refactor: centralize QR navigation logic and add export prompt after contact addition - Create QRNavigationService to handle platform-specific QR routing - Remove direct Capacitor imports from ContactsView, ProjectsView, HelpView - Replace duplicated QR routing logic with centralized service calls - Update HelpView template to use platform service methods (isCapacitor, capabilities) - Add export data prompt after successfully adding a contact - Add NOTIFY_EXPORT_DATA_PROMPT notification constant - Implement exportContactData() method with platform service integration - Fix TypeScript compatibility for Vue Router route parameters - Maintain consistent QR navigation behavior across all views Eliminates code duplication and improves platform abstraction by using PlatformService instead of direct Capacitor references. Enhances user experience with automatic export prompts for data backup. --- src/constants/notifications.ts | 6 ++ src/services/QRNavigationService.ts | 99 +++++++++++++++++++++++++++++ src/views/ContactsView.vue | 80 ++++++++++++++++++++--- src/views/HelpView.vue | 22 +++---- src/views/ProjectsView.vue | 12 ++-- 5 files changed, 194 insertions(+), 25 deletions(-) create mode 100644 src/services/QRNavigationService.ts diff --git a/src/constants/notifications.ts b/src/constants/notifications.ts index 9ffeb31f..8eb1c06f 100644 --- a/src/constants/notifications.ts +++ b/src/constants/notifications.ts @@ -846,6 +846,12 @@ export const NOTIFY_CONTACTS_ADDED = { message: "They were added.", }; +// Used in: ContactsView.vue (addContact method - export data prompt after contact addition) +export const NOTIFY_EXPORT_DATA_PROMPT = { + title: "Export Your Data", + message: "Would you like to export your contact data as a backup?", +}; + // Used in: ContactsView.vue (showCopySelectionsInfo method - info about copying contacts) export const NOTIFY_CONTACT_INFO_COPY = { title: "Info", diff --git a/src/services/QRNavigationService.ts b/src/services/QRNavigationService.ts new file mode 100644 index 00000000..33716c39 --- /dev/null +++ b/src/services/QRNavigationService.ts @@ -0,0 +1,99 @@ +import { PlatformServiceFactory } from "./PlatformServiceFactory"; +import { PlatformService } from "./PlatformService"; +import { logger } from "@/utils/logger"; + +/** + * QR Navigation Service + * + * Handles platform-specific routing logic for QR scanning operations. + * Removes coupling between views and routing logic by centralizing + * navigation decisions based on platform capabilities. + * + * @author Matthew Raymer + */ +export class QRNavigationService { + private static instance: QRNavigationService | null = null; + private platformService: PlatformService; + + private constructor() { + this.platformService = PlatformServiceFactory.getInstance(); + } + + /** + * Get singleton instance of QRNavigationService + */ + public static getInstance(): QRNavigationService { + if (!QRNavigationService.instance) { + QRNavigationService.instance = new QRNavigationService(); + } + return QRNavigationService.instance; + } + + /** + * Get the appropriate QR scanner route based on platform + * + * @returns Object with route name and parameters for QR scanning + */ + public getQRScannerRoute(): { + name: string; + params?: Record; + } { + const isCapacitor = this.platformService.isCapacitor(); + + logger.debug("QR Navigation - Platform detection:", { + isCapacitor, + platform: this.platformService.getCapabilities(), + }); + + if (isCapacitor) { + // Use native scanner on mobile platforms + return { name: "contact-qr-scan-full" }; + } else { + // Use web scanner on other platforms + return { name: "contact-qr" }; + } + } + + /** + * Get the appropriate QR display route based on platform + * + * @returns Object with route name and parameters for QR display + */ + public getQRDisplayRoute(): { + name: string; + params?: Record; + } { + const isCapacitor = this.platformService.isCapacitor(); + + logger.debug("QR Navigation - Display route detection:", { + isCapacitor, + platform: this.platformService.getCapabilities(), + }); + + if (isCapacitor) { + // Use dedicated display view on mobile + return { name: "contact-qr-scan-show" }; + } else { + // Use combined view on web + return { name: "contact-qr" }; + } + } + + /** + * Check if native QR scanning is available on current platform + * + * @returns true if native scanning is available, false otherwise + */ + public isNativeScanningAvailable(): boolean { + return this.platformService.isCapacitor(); + } + + /** + * Get platform capabilities for QR operations + * + * @returns Platform capabilities object + */ + public getPlatformCapabilities() { + return this.platformService.getCapabilities(); + } +} diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index 3d5576e7..a6533201 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -127,7 +127,7 @@ import * as R from "ramda"; import { Component, Vue } from "vue-facing-decorator"; import { RouteLocationNormalizedLoaded, Router } from "vue-router"; import { useClipboard } from "@vueuse/core"; -import { Capacitor } from "@capacitor/core"; +// Capacitor import removed - using PlatformService instead import QuickNav from "../components/QuickNav.vue"; import EntityIcon from "../components/EntityIcon.vue"; @@ -161,13 +161,17 @@ import { GiveSummaryRecord } from "@/interfaces/records"; import { UserInfo } from "@/interfaces/common"; import { VerifiableCredential } from "@/interfaces/claims-result"; import * as libsUtil from "../libs/util"; -import { generateSaveAndActivateIdentity } from "../libs/util"; +import { + generateSaveAndActivateIdentity, + contactsToExportJson, +} from "../libs/util"; import { logger } from "../utils/logger"; // No longer needed - using PlatformServiceMixin methods // import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; import { APP_SERVER } from "@/constants/app"; +import { QRNavigationService } from "@/services/QRNavigationService"; import { NOTIFY_CONTACT_NO_INFO, NOTIFY_CONTACTS_ADD_ERROR, @@ -193,6 +197,7 @@ import { NOTIFY_REGISTRATION_ERROR_FALLBACK, NOTIFY_REGISTRATION_ERROR_GENERIC, NOTIFY_VISIBILITY_ERROR_FALLBACK, + NOTIFY_EXPORT_DATA_PROMPT, getRegisterPersonSuccessMessage, getVisibilitySuccessMessage, getGivesRetrievalErrorMessage, @@ -780,6 +785,9 @@ export default class ContactsView extends Vue { // Show success notification this.notify.success(addedMessage); + + // Show export data prompt after successful contact addition + await this.showExportDataPrompt(); } catch (err) { this.handleContactAddError(err); } @@ -1243,19 +1251,75 @@ export default class ContactsView extends Vue { /** * Handle QR code button click - route to appropriate scanner - * Uses native scanner on mobile platforms, web scanner otherwise + * Uses QRNavigationService to determine scanner type and route */ - public handleQRCodeClick() { this.$logAndConsole( "[ContactsView] handleQRCodeClick method called", false, ); - if (Capacitor.isNativePlatform()) { - this.$router.push({ name: "contact-qr-scan-full" }); - } else { - this.$router.push({ name: "contact-qr" }); + const qrNavigationService = QRNavigationService.getInstance(); + const route = qrNavigationService.getQRScannerRoute(); + + this.$router.push(route); + } + + /** + * Show export data prompt after adding a contact + * Prompts user to export their contact data as a backup + */ + private async showExportDataPrompt(): Promise { + setTimeout(() => { + this.$notify( + { + group: "modal", + type: "confirm", + title: NOTIFY_EXPORT_DATA_PROMPT.title, + text: NOTIFY_EXPORT_DATA_PROMPT.message, + onYes: async () => { + await this.exportContactData(); + }, + yesText: "Export Data", + onNo: async () => { + // User chose not to export - no action needed + }, + noText: "Not Now", + }, + -1, + ); + }, 1000); // Small delay to ensure success notification is shown first + } + + /** + * Export contact data to JSON file + * Uses platform service to handle platform-specific export logic + */ + private async exportContactData(): Promise { + try { + // Fetch all contacts from database + const allContacts = await this.$contacts(); + + // Convert contacts to export format + const exportData = contactsToExportJson(allContacts); + const jsonStr = JSON.stringify(exportData, null, 2); + + // Generate filename with current date + const today = new Date(); + const dateString = today.toISOString().split("T")[0]; // YYYY-MM-DD format + const fileName = `timesafari-backup-contacts-${dateString}.json`; + + // Use platform service to handle export + await this.platformService.writeAndShareFile(fileName, jsonStr); + + this.notify.success( + "Contact export completed successfully. Check your downloads or share dialog.", + ); + } catch (error) { + logger.error("Export Error:", error); + this.notify.error( + `There was an error exporting the data: ${error instanceof Error ? error.message : "Unknown error"}`, + ); } } } diff --git a/src/views/HelpView.vue b/src/views/HelpView.vue index 11505ba3..9b05b7aa 100644 --- a/src/views/HelpView.vue +++ b/src/views/HelpView.vue @@ -565,22 +565,22 @@

What app version is this?

{{ package.version }} ({{ commitHash }})

-
+

Do I have the latest version?

-

+

Check the App Store.

-

+

Check the Play Store.

- Sorry, your platform of '{{ Capacitor.getPlatform() }}' is not recognized. + Sorry, your platform is not recognized.

@@ -592,12 +592,13 @@ import { Component, Vue } from "vue-facing-decorator"; import { Router } from "vue-router"; import { useClipboard } from "@vueuse/core"; -import { Capacitor } from "@capacitor/core"; +// Capacitor import removed - using QRNavigationService instead import * as Package from "../../package.json"; import QuickNav from "../components/QuickNav.vue"; import { APP_SERVER } from "../constants/app"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; +import { QRNavigationService } from "@/services/QRNavigationService"; /** * HelpView.vue - Comprehensive Help System Component @@ -643,7 +644,7 @@ export default class HelpView extends Vue { showVerifiable = false; APP_SERVER = APP_SERVER; - Capacitor = Capacitor; + // Capacitor reference removed - using QRNavigationService instead // Ideally, we put no functionality in here, especially in the setup, // because we never want this page to have a chance of throwing an error. @@ -711,11 +712,10 @@ export default class HelpView extends Vue { * @private */ private handleQRCodeClick(): void { - if (Capacitor.isNativePlatform()) { - this.$router.push({ name: "contact-qr-scan-full" }); - } else { - this.$router.push({ name: "contact-qr" }); - } + const qrNavigationService = QRNavigationService.getInstance(); + const route = qrNavigationService.getQRScannerRoute(); + + this.$router.push(route); } /** diff --git a/src/views/ProjectsView.vue b/src/views/ProjectsView.vue index 84db4f02..b2f2b7f4 100644 --- a/src/views/ProjectsView.vue +++ b/src/views/ProjectsView.vue @@ -264,7 +264,7 @@ import { AxiosRequestConfig } from "axios"; import { Component, Vue } from "vue-facing-decorator"; import { Router } from "vue-router"; -import { Capacitor } from "@capacitor/core"; +// Capacitor import removed - using QRNavigationService instead import { NotificationIface } from "../constants/app"; import EntityIcon from "../components/EntityIcon.vue"; @@ -281,6 +281,7 @@ import { OnboardPage, iconForUnitCode } from "../libs/util"; import { logger } from "../utils/logger"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; +import { QRNavigationService } from "@/services/QRNavigationService"; import { NOTIFY_NO_ACCOUNT_ERROR, NOTIFY_PROJECT_LOAD_ERROR, @@ -755,11 +756,10 @@ export default class ProjectsView extends Vue { * - Web-based QR interface for browser environments */ private handleQRCodeClick() { - if (Capacitor.isNativePlatform()) { - this.$router.push({ name: "contact-qr-scan-full" }); - } else { - this.$router.push({ name: "contact-qr" }); - } + const qrNavigationService = QRNavigationService.getInstance(); + const route = qrNavigationService.getQRScannerRoute(); + + this.$router.push(route); } /**