Browse Source
			
			
			
			
				
		- Deleted ContactScanView.vue and its route from the router. - Renamed ContactQRScanView.vue to ContactQRScanFullView.vue. - Updated all router paths, names, and references for consistency. - Fixed related links and imports to use the new view/component name.
				 5 changed files with 567 additions and 95 deletions
			
			
		@ -0,0 +1,430 @@ | 
				
			|||
<template> | 
				
			|||
  <!-- CONTENT --> | 
				
			|||
  <section id="Content" class="relativew-[100vw] h-[100vh]"> | 
				
			|||
    <div | 
				
			|||
      class="absolute inset-x-0 bottom-0 bg-black/50 p-6 pb-[calc(env(safe-area-inset-bottom)+1.5rem)]" | 
				
			|||
    > | 
				
			|||
      <p class="text-center text-white mb-3"> | 
				
			|||
        Point your camera at a TimeSafari contact QR code to scan it | 
				
			|||
        automatically. | 
				
			|||
      </p> | 
				
			|||
 | 
				
			|||
      <p v-if="error" class="text-center text-rose-300 mb-3">{{ error }}</p> | 
				
			|||
 | 
				
			|||
      <div class="flex justify-center items-center"> | 
				
			|||
        <button | 
				
			|||
          class="text-center text-slate-600 leading-none bg-white p-2 rounded-full drop-shadow-lg" | 
				
			|||
          @click="handleBack" | 
				
			|||
        > | 
				
			|||
          <font-awesome icon="xmark" class="size-6"></font-awesome> | 
				
			|||
        </button> | 
				
			|||
      </div> | 
				
			|||
    </div> | 
				
			|||
  </section> | 
				
			|||
</template> | 
				
			|||
 | 
				
			|||
<script lang="ts"> | 
				
			|||
import { Component, Vue } from "vue-facing-decorator"; | 
				
			|||
import { Router } from "vue-router"; | 
				
			|||
import { logger } from "../utils/logger"; | 
				
			|||
import { QRScannerFactory } from "../services/QRScanner/QRScannerFactory"; | 
				
			|||
import QuickNav from "../components/QuickNav.vue"; | 
				
			|||
import { NotificationIface } from "../constants/app"; | 
				
			|||
import { db } from "../db/index"; | 
				
			|||
import { Contact } from "../db/tables/contacts"; | 
				
			|||
import { getContactJwtFromJwtUrl } from "../libs/crypto"; | 
				
			|||
import { decodeEndorserJwt } from "../libs/crypto/vc"; | 
				
			|||
import { retrieveSettingsForActiveAccount } from "../db/index"; | 
				
			|||
import { setVisibilityUtil } from "../libs/endorserServer"; | 
				
			|||
 | 
				
			|||
interface QRScanResult { | 
				
			|||
  rawValue?: string; | 
				
			|||
  barcode?: string; | 
				
			|||
} | 
				
			|||
 | 
				
			|||
@Component({ | 
				
			|||
  components: { | 
				
			|||
    QuickNav, | 
				
			|||
  }, | 
				
			|||
}) | 
				
			|||
export default class ContactQRScan extends Vue { | 
				
			|||
  $notify!: (notification: NotificationIface, timeout?: number) => void; | 
				
			|||
  $router!: Router; | 
				
			|||
 | 
				
			|||
  isScanning = false; | 
				
			|||
  error: string | null = null; | 
				
			|||
  activeDid = ""; | 
				
			|||
  apiServer = ""; | 
				
			|||
 | 
				
			|||
  // Add new properties to track scanning state | 
				
			|||
  private lastScannedValue: string = ""; | 
				
			|||
  private lastScanTime: number = 0; | 
				
			|||
  private readonly SCAN_DEBOUNCE_MS = 2000; // Prevent duplicate scans within 2 seconds | 
				
			|||
 | 
				
			|||
  // Add cleanup tracking | 
				
			|||
  private isCleaningUp = false; | 
				
			|||
  private isMounted = false; | 
				
			|||
 | 
				
			|||
  async created() { | 
				
			|||
    try { | 
				
			|||
      const settings = await retrieveSettingsForActiveAccount(); | 
				
			|||
      this.activeDid = settings.activeDid || ""; | 
				
			|||
      this.apiServer = settings.apiServer || ""; | 
				
			|||
    } catch (error) { | 
				
			|||
      logger.error("Error initializing component:", { | 
				
			|||
        error: error instanceof Error ? error.message : String(error), | 
				
			|||
        stack: error instanceof Error ? error.stack : undefined, | 
				
			|||
      }); | 
				
			|||
      this.$notify({ | 
				
			|||
        group: "alert", | 
				
			|||
        type: "danger", | 
				
			|||
        title: "Initialization Error", | 
				
			|||
        text: "Failed to initialize QR scanner. Please try again.", | 
				
			|||
      }); | 
				
			|||
    } | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  async startScanning() { | 
				
			|||
    if (this.isCleaningUp) { | 
				
			|||
      logger.debug("Cannot start scanning during cleanup"); | 
				
			|||
      return; | 
				
			|||
    } | 
				
			|||
 | 
				
			|||
    try { | 
				
			|||
      this.error = null; | 
				
			|||
      this.isScanning = true; | 
				
			|||
      this.lastScannedValue = ""; | 
				
			|||
      this.lastScanTime = 0; | 
				
			|||
 | 
				
			|||
      const scanner = QRScannerFactory.getInstance(); | 
				
			|||
 | 
				
			|||
      // Check if scanning is supported first | 
				
			|||
      if (!(await scanner.isSupported())) { | 
				
			|||
        this.error = | 
				
			|||
          "Camera access requires HTTPS. Please use a secure connection."; | 
				
			|||
        this.isScanning = false; | 
				
			|||
        this.$notify( | 
				
			|||
          { | 
				
			|||
            group: "alert", | 
				
			|||
            type: "warning", | 
				
			|||
            title: "HTTPS Required", | 
				
			|||
            text: "Camera access requires a secure (HTTPS) connection", | 
				
			|||
          }, | 
				
			|||
          5000, | 
				
			|||
        ); | 
				
			|||
        return; | 
				
			|||
      } | 
				
			|||
 | 
				
			|||
      // Check permissions first | 
				
			|||
      if (!(await scanner.checkPermissions())) { | 
				
			|||
        const granted = await scanner.requestPermissions(); | 
				
			|||
        if (!granted) { | 
				
			|||
          this.error = "Camera permission denied"; | 
				
			|||
          this.isScanning = false; | 
				
			|||
          // Show notification for better visibility | 
				
			|||
          this.$notify( | 
				
			|||
            { | 
				
			|||
              group: "alert", | 
				
			|||
              type: "warning", | 
				
			|||
              title: "Camera Access Required", | 
				
			|||
              text: "Camera permission denied", | 
				
			|||
            }, | 
				
			|||
            5000, | 
				
			|||
          ); | 
				
			|||
          return; | 
				
			|||
        } | 
				
			|||
      } | 
				
			|||
 | 
				
			|||
      // Add scan listener | 
				
			|||
      scanner.addListener({ | 
				
			|||
        onScan: this.onScanDetect, | 
				
			|||
        onError: this.onScanError, | 
				
			|||
      }); | 
				
			|||
 | 
				
			|||
      // Start scanning | 
				
			|||
      await scanner.startScan(); | 
				
			|||
    } catch (error) { | 
				
			|||
      this.error = error instanceof Error ? error.message : String(error); | 
				
			|||
      this.isScanning = false; | 
				
			|||
      logger.error("Error starting scan:", { | 
				
			|||
        error: error instanceof Error ? error.message : String(error), | 
				
			|||
        stack: error instanceof Error ? error.stack : undefined, | 
				
			|||
      }); | 
				
			|||
    } | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  async stopScanning() { | 
				
			|||
    try { | 
				
			|||
      const scanner = QRScannerFactory.getInstance(); | 
				
			|||
      await scanner.stopScan(); | 
				
			|||
    } catch (error) { | 
				
			|||
      logger.error("Error stopping scan:", { | 
				
			|||
        error: error instanceof Error ? error.message : String(error), | 
				
			|||
        stack: error instanceof Error ? error.stack : undefined, | 
				
			|||
      }); | 
				
			|||
    } finally { | 
				
			|||
      this.isScanning = false; | 
				
			|||
      this.lastScannedValue = ""; | 
				
			|||
      this.lastScanTime = 0; | 
				
			|||
    } | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  async cleanupScanner() { | 
				
			|||
    if (this.isCleaningUp) { | 
				
			|||
      return; | 
				
			|||
    } | 
				
			|||
 | 
				
			|||
    this.isCleaningUp = true; | 
				
			|||
    try { | 
				
			|||
      logger.info("Cleaning up QR scanner resources"); | 
				
			|||
      await this.stopScanning(); | 
				
			|||
      await QRScannerFactory.cleanup(); | 
				
			|||
    } catch (error) { | 
				
			|||
      logger.error("Error during scanner cleanup:", { | 
				
			|||
        error: error instanceof Error ? error.message : String(error), | 
				
			|||
        stack: error instanceof Error ? error.stack : undefined, | 
				
			|||
      }); | 
				
			|||
    } finally { | 
				
			|||
      this.isCleaningUp = false; | 
				
			|||
    } | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  /** | 
				
			|||
   * Handle QR code scan result with debouncing to prevent duplicate scans | 
				
			|||
   */ | 
				
			|||
  async onScanDetect(result: string | QRScanResult) { | 
				
			|||
    try { | 
				
			|||
      // Extract raw value from different possible formats | 
				
			|||
      const rawValue = | 
				
			|||
        typeof result === "string" | 
				
			|||
          ? result | 
				
			|||
          : result?.rawValue || result?.barcode; | 
				
			|||
      if (!rawValue) { | 
				
			|||
        logger.warn("Invalid scan result - no value found:", result); | 
				
			|||
        return; | 
				
			|||
      } | 
				
			|||
 | 
				
			|||
      // Debounce duplicate scans | 
				
			|||
      const now = Date.now(); | 
				
			|||
      if ( | 
				
			|||
        rawValue === this.lastScannedValue && | 
				
			|||
        now - this.lastScanTime < this.SCAN_DEBOUNCE_MS | 
				
			|||
      ) { | 
				
			|||
        logger.info("Ignoring duplicate scan:", rawValue); | 
				
			|||
        return; | 
				
			|||
      } | 
				
			|||
 | 
				
			|||
      // Update scan tracking | 
				
			|||
      this.lastScannedValue = rawValue; | 
				
			|||
      this.lastScanTime = now; | 
				
			|||
 | 
				
			|||
      logger.info("Processing QR code scan result:", rawValue); | 
				
			|||
 | 
				
			|||
      // Extract JWT | 
				
			|||
      const jwt = getContactJwtFromJwtUrl(rawValue); | 
				
			|||
      if (!jwt) { | 
				
			|||
        logger.warn("Invalid QR code format - no JWT found in URL"); | 
				
			|||
        this.$notify({ | 
				
			|||
          group: "alert", | 
				
			|||
          type: "danger", | 
				
			|||
          title: "Invalid QR Code", | 
				
			|||
          text: "This QR code does not contain valid contact information. Please scan a TimeSafari contact QR code.", | 
				
			|||
        }); | 
				
			|||
        return; | 
				
			|||
      } | 
				
			|||
 | 
				
			|||
      // Process JWT and contact info | 
				
			|||
      logger.info("Decoding JWT payload from QR code"); | 
				
			|||
      const decodedJwt = await decodeEndorserJwt(jwt); | 
				
			|||
      if (!decodedJwt?.payload?.own) { | 
				
			|||
        logger.warn("Invalid JWT payload - missing 'own' field"); | 
				
			|||
        this.$notify({ | 
				
			|||
          group: "alert", | 
				
			|||
          type: "danger", | 
				
			|||
          title: "Invalid Contact Info", | 
				
			|||
          text: "The contact information is incomplete or invalid.", | 
				
			|||
        }); | 
				
			|||
        return; | 
				
			|||
      } | 
				
			|||
 | 
				
			|||
      const contactInfo = decodedJwt.payload.own; | 
				
			|||
      if (!contactInfo.did) { | 
				
			|||
        logger.warn("Invalid contact info - missing DID"); | 
				
			|||
        this.$notify({ | 
				
			|||
          group: "alert", | 
				
			|||
          type: "danger", | 
				
			|||
          title: "Invalid Contact", | 
				
			|||
          text: "The contact DID is missing.", | 
				
			|||
        }); | 
				
			|||
        return; | 
				
			|||
      } | 
				
			|||
 | 
				
			|||
      // Create contact object | 
				
			|||
      const contact = { | 
				
			|||
        did: contactInfo.did, | 
				
			|||
        name: contactInfo.name || "", | 
				
			|||
        email: contactInfo.email || "", | 
				
			|||
        phone: contactInfo.phone || "", | 
				
			|||
        company: contactInfo.company || "", | 
				
			|||
        title: contactInfo.title || "", | 
				
			|||
        notes: contactInfo.notes || "", | 
				
			|||
      }; | 
				
			|||
 | 
				
			|||
      // Add contact and stop scanning | 
				
			|||
      logger.info("Adding new contact to database:", { | 
				
			|||
        did: contact.did, | 
				
			|||
        name: contact.name, | 
				
			|||
      }); | 
				
			|||
      await this.addNewContact(contact); | 
				
			|||
      await this.stopScanning(); | 
				
			|||
      this.$router.back(); // Return to previous view after successful scan | 
				
			|||
    } catch (error) { | 
				
			|||
      logger.error("Error processing contact QR code:", { | 
				
			|||
        error: error instanceof Error ? error.message : String(error), | 
				
			|||
        stack: error instanceof Error ? error.stack : undefined, | 
				
			|||
      }); | 
				
			|||
      this.$notify({ | 
				
			|||
        group: "alert", | 
				
			|||
        type: "danger", | 
				
			|||
        title: "Error", | 
				
			|||
        text: | 
				
			|||
          error instanceof Error | 
				
			|||
            ? error.message | 
				
			|||
            : "Could not process QR code. Please try again.", | 
				
			|||
      }); | 
				
			|||
    } | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  onScanError(error: Error) { | 
				
			|||
    this.error = error.message; | 
				
			|||
    logger.error("QR code scan error:", { | 
				
			|||
      error: error.message, | 
				
			|||
      stack: error.stack, | 
				
			|||
    }); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  async setVisibility(contact: Contact, visibility: boolean) { | 
				
			|||
    const result = await setVisibilityUtil( | 
				
			|||
      this.activeDid, | 
				
			|||
      this.apiServer, | 
				
			|||
      this.axios, | 
				
			|||
      db, | 
				
			|||
      contact, | 
				
			|||
      visibility, | 
				
			|||
    ); | 
				
			|||
    if (result.error) { | 
				
			|||
      this.$notify({ | 
				
			|||
        group: "alert", | 
				
			|||
        type: "danger", | 
				
			|||
        title: "Error Setting Visibility", | 
				
			|||
        text: result.error as string, | 
				
			|||
      }); | 
				
			|||
    } else if (!result.success) { | 
				
			|||
      logger.warn("Unexpected result from setting visibility:", result); | 
				
			|||
    } | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  async addNewContact(contact: Contact) { | 
				
			|||
    try { | 
				
			|||
      logger.info("Opening database connection for new contact"); | 
				
			|||
      await db.open(); | 
				
			|||
 | 
				
			|||
      // Check if contact already exists | 
				
			|||
      const existingContacts = await db.contacts.toArray(); | 
				
			|||
      const existingContact = existingContacts.find( | 
				
			|||
        (c) => c.did === contact.did, | 
				
			|||
      ); | 
				
			|||
 | 
				
			|||
      if (existingContact) { | 
				
			|||
        logger.info("Contact already exists", { did: contact.did }); | 
				
			|||
        this.$notify( | 
				
			|||
          { | 
				
			|||
            group: "alert", | 
				
			|||
            type: "warning", | 
				
			|||
            title: "Contact Exists", | 
				
			|||
            text: "This contact has already been added to your list.", | 
				
			|||
          }, | 
				
			|||
          3000, | 
				
			|||
        ); | 
				
			|||
        return; | 
				
			|||
      } | 
				
			|||
 | 
				
			|||
      // Add new contact | 
				
			|||
      await db.contacts.add(contact); | 
				
			|||
 | 
				
			|||
      if (this.activeDid) { | 
				
			|||
        logger.info("Setting contact visibility", { did: contact.did }); | 
				
			|||
        await this.setVisibility(contact, true); | 
				
			|||
        contact.seesMe = true; | 
				
			|||
      } | 
				
			|||
 | 
				
			|||
      this.$notify( | 
				
			|||
        { | 
				
			|||
          group: "alert", | 
				
			|||
          type: "success", | 
				
			|||
          title: "Contact Added", | 
				
			|||
          text: this.activeDid | 
				
			|||
            ? "They were added, and your activity is visible to them." | 
				
			|||
            : "They were added.", | 
				
			|||
        }, | 
				
			|||
        3000, | 
				
			|||
      ); | 
				
			|||
    } catch (error) { | 
				
			|||
      logger.error("Error saving contact to database:", { | 
				
			|||
        did: contact.did, | 
				
			|||
        error: error instanceof Error ? error.message : String(error), | 
				
			|||
        stack: error instanceof Error ? error.stack : undefined, | 
				
			|||
      }); | 
				
			|||
      this.$notify( | 
				
			|||
        { | 
				
			|||
          group: "alert", | 
				
			|||
          type: "danger", | 
				
			|||
          title: "Contact Error", | 
				
			|||
          text: "Could not save contact. Check if it already exists.", | 
				
			|||
        }, | 
				
			|||
        5000, | 
				
			|||
      ); | 
				
			|||
    } | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  // Lifecycle hooks | 
				
			|||
  mounted() { | 
				
			|||
    this.isMounted = true; | 
				
			|||
    document.addEventListener("pause", this.handleAppPause); | 
				
			|||
    document.addEventListener("resume", this.handleAppResume); | 
				
			|||
    this.startScanning(); // Automatically start scanning when view is mounted | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  beforeDestroy() { | 
				
			|||
    this.isMounted = false; | 
				
			|||
    document.removeEventListener("pause", this.handleAppPause); | 
				
			|||
    document.removeEventListener("resume", this.handleAppResume); | 
				
			|||
    this.cleanupScanner(); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  async handleAppPause() { | 
				
			|||
    if (!this.isMounted) return; | 
				
			|||
 | 
				
			|||
    logger.info("App paused, stopping scanner"); | 
				
			|||
    await this.stopScanning(); | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  handleAppResume() { | 
				
			|||
    if (!this.isMounted) return; | 
				
			|||
 | 
				
			|||
    logger.info("App resumed, scanner can be restarted by user"); | 
				
			|||
    this.isScanning = false; | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  async handleBack() { | 
				
			|||
    await this.cleanupScanner(); | 
				
			|||
    this.$router.back(); | 
				
			|||
  } | 
				
			|||
} | 
				
			|||
</script> | 
				
			|||
 | 
				
			|||
<style scoped> | 
				
			|||
.aspect-square { | 
				
			|||
  aspect-ratio: 1 / 1; | 
				
			|||
} | 
				
			|||
</style> | 
				
			|||
					Loading…
					
					
				
		Reference in new issue