diff --git a/android/.gradle/file-system.probe b/android/.gradle/file-system.probe
index 44390a33..c35ad2ac 100644
Binary files a/android/.gradle/file-system.probe and b/android/.gradle/file-system.probe differ
diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle
index a54399fa..9a925acf 100644
--- a/android/app/capacitor.build.gradle
+++ b/android/app/capacitor.build.gradle
@@ -11,6 +11,7 @@ apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
 dependencies {
     implementation project(':capacitor-app')
     implementation project(':capacitor-camera')
+    implementation project(':capacitor-clipboard')
     implementation project(':capacitor-filesystem')
     implementation project(':capacitor-share')
     implementation project(':capawesome-capacitor-file-picker')
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 70ac8410..a95eb6e7 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -41,4 +41,5 @@
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.CAMERA" />
 </manifest>
diff --git a/android/app/src/main/assets/capacitor.plugins.json b/android/app/src/main/assets/capacitor.plugins.json
index 30b5ba98..6a10948b 100644
--- a/android/app/src/main/assets/capacitor.plugins.json
+++ b/android/app/src/main/assets/capacitor.plugins.json
@@ -7,6 +7,10 @@
 		"pkg": "@capacitor/camera",
 		"classpath": "com.capacitorjs.plugins.camera.CameraPlugin"
 	},
+	{
+		"pkg": "@capacitor/clipboard",
+		"classpath": "com.capacitorjs.plugins.clipboard.ClipboardPlugin"
+	},
 	{
 		"pkg": "@capacitor/filesystem",
 		"classpath": "com.capacitorjs.plugins.filesystem.FilesystemPlugin"
diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle
index 736eac60..77dd6f02 100644
--- a/android/capacitor.settings.gradle
+++ b/android/capacitor.settings.gradle
@@ -8,6 +8,9 @@ project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/
 include ':capacitor-camera'
 project(':capacitor-camera').projectDir = new File('../node_modules/@capacitor/camera/android')
 
+include ':capacitor-clipboard'
+project(':capacitor-clipboard').projectDir = new File('../node_modules/@capacitor/clipboard/android')
+
 include ':capacitor-filesystem'
 project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android')
 
diff --git a/package-lock.json b/package-lock.json
index dd158e30..38916b28 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
         "@capacitor/app": "^6.0.0",
         "@capacitor/camera": "^6.0.0",
         "@capacitor/cli": "^6.2.0",
+        "@capacitor/clipboard": "^6.0.2",
         "@capacitor/core": "^6.2.0",
         "@capacitor/filesystem": "^6.0.0",
         "@capacitor/ios": "^6.2.0",
@@ -2882,6 +2883,15 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
+    "node_modules/@capacitor/clipboard": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/@capacitor/clipboard/-/clipboard-6.0.2.tgz",
+      "integrity": "sha512-jQ6UeFra5NP58THNZNb7HtzOZU7cHsjgrbQGVuMTgsK1uTILZpNeh+pfqHbKggba6KaNh5DAsJvEVQGpIR1VBA==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@capacitor/core": "^6.0.0"
+      }
+    },
     "node_modules/@capacitor/core": {
       "version": "6.2.1",
       "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.2.1.tgz",
diff --git a/package.json b/package.json
index 686caad4..78c6b859 100644
--- a/package.json
+++ b/package.json
@@ -6,10 +6,13 @@
     "name": "Time Safari Team"
   },
   "scripts": {
-    "dev": "vite --config vite.config.dev.mts",
+    "dev": "vite",
     "serve": "vite preview",
-    "build": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.mts",
-    "lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src",
+    "build": "vite build",
+    "build:mobile": "VITE_PLATFORM=capacitor vite build",
+    "preview": "vite preview",
+    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
+    "format": "prettier --write src/",
     "lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src",
     "prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js",
     "test:all": "npm run test:prerequisites && npm run build && npm run test:web && npm run test:mobile",
@@ -47,6 +50,7 @@
     "@capacitor/app": "^6.0.0",
     "@capacitor/camera": "^6.0.0",
     "@capacitor/cli": "^6.2.0",
+    "@capacitor/clipboard": "^6.0.2",
     "@capacitor/core": "^6.2.0",
     "@capacitor/filesystem": "^6.0.0",
     "@capacitor/ios": "^6.2.0",
diff --git a/src/services/PlatformService.ts b/src/services/PlatformService.ts
index 574b1a3a..adfd8012 100644
--- a/src/services/PlatformService.ts
+++ b/src/services/PlatformService.ts
@@ -98,4 +98,18 @@ export interface PlatformService {
    * @returns Promise that resolves when the deep link has been handled
    */
   handleDeepLink(url: string): Promise<void>;
+
+  // Clipboard operations
+  /**
+   * Writes text to the system clipboard.
+   * @param text - The text to write to the clipboard
+   * @returns Promise that resolves when the write is complete
+   */
+  writeToClipboard(text: string): Promise<void>;
+
+  /**
+   * Reads text from the system clipboard.
+   * @returns Promise resolving to the clipboard text
+   */
+  readFromClipboard(): Promise<string>;
 }
diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts
index 9c655eb6..2fb9b8e4 100644
--- a/src/services/platforms/CapacitorPlatformService.ts
+++ b/src/services/platforms/CapacitorPlatformService.ts
@@ -7,6 +7,7 @@ import { Filesystem, Directory, Encoding } from "@capacitor/filesystem";
 import { Camera, CameraResultType, CameraSource } from "@capacitor/camera";
 import { Share } from "@capacitor/share";
 import { logger } from "../../utils/logger";
+import { Clipboard } from "@capacitor/clipboard";
 
 /**
  * Platform service implementation for Capacitor (mobile) platform.
@@ -318,12 +319,12 @@ export class CapacitorPlatformService implements PlatformService {
   async writeAndShareFile(fileName: string, content: string): Promise<void> {
     const timestamp = new Date().toISOString();
     const logData = {
-      action: 'writeAndShareFile',
+      action: "writeAndShareFile",
       fileName,
       contentLength: content.length,
       timestamp,
     };
-    logger.log('[CapacitorPlatformService]', JSON.stringify(logData, null, 2));
+    logger.log("[CapacitorPlatformService]", JSON.stringify(logData, null, 2));
 
     try {
       const { uri } = await Filesystem.writeFile({
@@ -334,13 +335,16 @@ export class CapacitorPlatformService implements PlatformService {
         recursive: true,
       });
 
-      logger.log('[CapacitorPlatformService] File write successful:', { uri, timestamp: new Date().toISOString() });
+      logger.log("[CapacitorPlatformService] File write successful:", {
+        uri,
+        timestamp: new Date().toISOString(),
+      });
 
       await Share.share({
-        title: 'TimeSafari Backup',
-        text: 'Here is your backup file.',
+        title: "TimeSafari Backup",
+        text: "Here is your backup file.",
         url: uri,
-        dialogTitle: 'Share your backup file',
+        dialogTitle: "Share your backup file",
       });
     } catch (error) {
       const err = error as Error;
@@ -349,7 +353,10 @@ export class CapacitorPlatformService implements PlatformService {
         stack: err.stack,
         timestamp: new Date().toISOString(),
       };
-      logger.error('[CapacitorPlatformService] Error writing or sharing file:', JSON.stringify(errLog, null, 2));
+      logger.error(
+        "[CapacitorPlatformService] Error writing or sharing file:",
+        JSON.stringify(errLog, null, 2),
+      );
       throw new Error(`Failed to write or share file: ${err.message}`);
     }
   }
@@ -470,4 +477,34 @@ export class CapacitorPlatformService implements PlatformService {
     // This is just a placeholder for the interface
     return Promise.resolve();
   }
+
+  /**
+   * Writes text to the system clipboard using Capacitor's Clipboard plugin.
+   * @param text - The text to write to the clipboard
+   * @returns Promise that resolves when the write is complete
+   */
+  async writeToClipboard(text: string): Promise<void> {
+    try {
+      await Clipboard.write({
+        string: text,
+      });
+    } catch (error) {
+      logger.error("Error writing to clipboard:", error);
+      throw new Error("Failed to write to clipboard");
+    }
+  }
+
+  /**
+   * Reads text from the system clipboard using Capacitor's Clipboard plugin.
+   * @returns Promise resolving to the clipboard text
+   */
+  async readFromClipboard(): Promise<string> {
+    try {
+      const { value } = await Clipboard.read();
+      return value;
+    } catch (error) {
+      logger.error("Error reading from clipboard:", error);
+      throw new Error("Failed to read from clipboard");
+    }
+  }
 }
diff --git a/src/services/platforms/WebPlatformService.ts b/src/services/platforms/WebPlatformService.ts
index 8b911a92..591e8883 100644
--- a/src/services/platforms/WebPlatformService.ts
+++ b/src/services/platforms/WebPlatformService.ts
@@ -228,4 +228,31 @@ export class WebPlatformService implements PlatformService {
     // Web platform can handle deep links through URL parameters
     return Promise.resolve();
   }
+
+  /**
+   * Writes text to the system clipboard using the Web Clipboard API.
+   * @param text - The text to write to the clipboard
+   * @returns Promise that resolves when the write is complete
+   */
+  async writeToClipboard(text: string): Promise<void> {
+    try {
+      await navigator.clipboard.writeText(text);
+    } catch (error) {
+      logger.error("Error writing to clipboard:", error);
+      throw new Error("Failed to write to clipboard");
+    }
+  }
+
+  /**
+   * Reads text from the system clipboard using the Web Clipboard API.
+   * @returns Promise resolving to the clipboard text
+   */
+  async readFromClipboard(): Promise<string> {
+    try {
+      return await navigator.clipboard.readText();
+    } catch (error) {
+      logger.error("Error reading from clipboard:", error);
+      throw new Error("Failed to read from clipboard");
+    }
+  }
 }
diff --git a/src/views/ContactQRScanShowView.vue b/src/views/ContactQRScanShowView.vue
index f315e658..5d5563cf 100644
--- a/src/views/ContactQRScanShowView.vue
+++ b/src/views/ContactQRScanShowView.vue
@@ -77,11 +77,24 @@
 
     <div class="text-center">
       <h1 class="text-4xl text-center font-light pt-6">Scan Contact Info</h1>
-      <qrcode-stream @detect="onScanDetect" @error="onScanError" />
-      <span>
-        If you do not see a scanning camera window here, check your camera
-        permissions.
-      </span>
+      <!-- Web QR Code Scanner -->
+      <qrcode-stream
+        v-if="useQRReader"
+        @detect="onScanDetect"
+        @error="onScanError"
+      />
+      <!-- Mobile Camera Button -->
+      <div v-else class="mt-4">
+        <button
+          class="bg-blue-500 text-white px-4 py-2 rounded-md"
+          @click="openMobileCamera"
+        >
+          Open Camera
+        </button>
+        <p class="mt-2 text-sm text-gray-600">
+          If you do not see the camera, check your camera permissions.
+        </p>
+      </div>
     </div>
   </section>
 </template>
@@ -91,8 +104,8 @@ import { AxiosError } from "axios";
 import QRCodeVue3 from "qr-code-generator-vue3";
 import { Component, Vue } from "vue-facing-decorator";
 import { QrcodeStream } from "vue-qrcode-reader";
-import { useClipboard } from "@vueuse/core";
-
+import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
+import { PlatformService } from "../services/PlatformService";
 import QuickNav from "../components/QuickNav.vue";
 import UserNameDialog from "../components/UserNameDialog.vue";
 import { NotificationIface } from "../constants/app";
@@ -110,9 +123,15 @@ import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
 import { retrieveAccountMetadata } from "../libs/util";
 import { Router } from "vue-router";
 import { logger } from "../utils/logger";
+import { Camera, CameraResultType, CameraSource } from "@capacitor/camera";
+
+// Declare global constants
+declare const __USE_QR_READER__: boolean;
+declare const __IS_MOBILE__: boolean;
+
 @Component({
   components: {
-    QrcodeStream,
+    QrcodeStream: __USE_QR_READER__ ? QrcodeStream : null,
     QRCodeVue3,
     QuickNav,
     UserNameDialog,
@@ -121,6 +140,11 @@ import { logger } from "../utils/logger";
 export default class ContactQRScanShow extends Vue {
   $notify!: (notification: NotificationIface, timeout?: number) => void;
   $router!: Router;
+  declare $refs: {
+    userNameDialog: {
+      open: (callback: (name: string) => void) => void;
+    };
+  };
 
   activeDid = "";
   apiServer = "";
@@ -131,6 +155,9 @@ export default class ContactQRScanShow extends Vue {
 
   ETHR_DID_PREFIX = ETHR_DID_PREFIX;
 
+  private platformService: PlatformService =
+    PlatformServiceFactory.getInstance();
+
   async created() {
     const settings = await retrieveSettingsForActiveAccount();
     this.activeDid = settings.activeDid || "";
@@ -144,19 +171,149 @@ export default class ContactQRScanShow extends Vue {
     if (account) {
       const name =
         (settings.firstName || "") +
-        (settings.lastName ? ` ${settings.lastName}` : ""); // lastName is deprecated, pre v 0.1.3
+        (settings.lastName ? ` ${settings.lastName}` : "");
 
       this.qrValue = await generateEndorserJwtUrlForAccount(
         account,
         !!settings.isRegistered,
         name,
-        settings.profileImageUrl,
+        settings.profileImageUrl || "",
         false,
       );
     }
+
+    // Initialize camera with retry logic
+    if (this.useQRReader) {
+      await this.initializeCamera();
+    }
+  }
+
+  async initializeCamera(retryCount = 0): Promise<void> {
+    try {
+      const capabilities = this.platformService.getCapabilities();
+      if (!capabilities.hasCamera) {
+        this.danger("No camera available on this device.", "Camera Error");
+        return;
+      }
+
+      // Check camera permissions
+      const hasPermission = await this.checkCameraPermission();
+      if (!hasPermission) {
+        this.danger(
+          "Camera permission is required to scan QR codes. Please enable camera access in your device settings.",
+          "Permission Required"
+        );
+        return;
+      }
+
+      // If we get here, camera should be available
+      this.$notify(
+        {
+          group: "alert",
+          type: "success",
+          title: "Camera Ready",
+          text: "Camera is ready to scan QR codes.",
+        },
+        3000
+      );
+    } catch (error) {
+      logger.error("Error initializing camera:", error);
+      
+      // Retry up to 3 times for certain errors
+      if (retryCount < 3) {
+        const isPermissionError = error instanceof Error && 
+          (error.message.includes("permission") || 
+           error.message.includes("NotReadableError"));
+        
+        if (isPermissionError) {
+          // Wait before retrying
+          await new Promise(resolve => setTimeout(resolve, 1000));
+          return this.initializeCamera(retryCount + 1);
+        }
+      }
+
+      this.danger(
+        "Failed to initialize camera. Please check your camera permissions and try again.",
+        "Camera Error"
+      );
+    }
+  }
+
+  async checkCameraPermission(): Promise<boolean> {
+    try {
+      const capabilities = this.platformService.getCapabilities();
+      if (!capabilities.hasCamera) {
+        return false;
+      }
+
+      // Try to access camera to check permissions
+      await this.platformService.takePicture();
+      return true;
+    } catch (error) {
+      logger.error("Camera permission check failed:", error);
+      return false;
+    }
+  }
+
+  async openMobileCamera(): Promise<void> {
+    try {
+      // Check permissions first
+      const hasPermission = await this.checkCameraPermission();
+      if (!hasPermission) {
+        this.danger(
+          "Camera permission is required. Please enable camera access in your device settings.",
+          "Permission Required"
+        );
+        return;
+      }
+
+      const image = await Camera.getPhoto({
+        quality: 90,
+        allowEditing: false,
+        resultType: CameraResultType.DataUrl,
+        source: CameraSource.Camera,
+      });
+
+      if (image.dataUrl) {
+        await this.processImageForQRCode(image.dataUrl);
+      }
+    } catch (error) {
+      logger.error("Error taking picture:", error);
+      this.danger(
+        "Failed to access camera. Please check your camera permissions.",
+        "Camera Error"
+      );
+    }
   }
 
-  danger(message: string, title: string = "Error", timeout = 5000) {
+  async processImageForQRCode(_imageDataUrl: string) {
+    try {
+      // Here you would implement QR code scanning from the image
+      // For example, using jsQR:
+      // const image = new Image();
+      // image.src = imageDataUrl;
+      // image.onload = () => {
+      //   const canvas = document.createElement('canvas');
+      //   const context = canvas.getContext('2d');
+      //   canvas.width = image.width;
+      //   canvas.height = image.height;
+      //   context.drawImage(image, 0, 0);
+      //   const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
+      //   const code = jsQR(imageData.data, imageData.width, imageData.height);
+      //   if (code) {
+      //     this.onScanDetect([{ rawValue: code.data }]);
+      //   }
+      // };
+    } catch (error) {
+      logger.error("Error processing image for QR code:", error);
+      this.danger(
+        "Failed to process the image. Please try again.",
+        "Processing Error",
+      );
+    }
+  }
+
+  danger(message: string, title = "Error", timeout = 5000): void {
     this.$notify(
       {
         group: "alert",
@@ -174,131 +331,157 @@ export default class ContactQRScanShow extends Vue {
    */
   // Unfortunately, there are not typescript definitions for the qrcode-stream component yet.
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  async onScanDetect(content: any) {
+  async onScanDetect(content: any): Promise<void> {
     const url = content[0]?.rawValue;
-    if (url) {
-      let newContact: Contact;
-      try {
-        const jwt = getContactJwtFromJwtUrl(url);
-        if (!jwt) {
-          this.$notify(
-            {
-              group: "alert",
-              type: "danger",
-              title: "No Contact Info",
-              text: "The contact info could not be parsed.",
-            },
-            3000,
-          );
-          return;
-        }
-        const { payload } = decodeEndorserJwt(jwt);
-        newContact = {
-          did: payload.own.did || payload.iss, // ".own.did" is reliable as of v 0.3.49
-          name: payload.own.name,
-          nextPubKeyHashB64: payload.own.nextPublicEncKeyHash,
-          profileImageUrl: payload.own.profileImageUrl,
-          publicKeyBase64: payload.own.publicEncKey,
-          registered: payload.own.registered,
-        };
-        if (!newContact.did) {
-          this.danger("There is no DID.", "Incomplete Contact");
-          return;
-        }
-        if (!isDid(newContact.did)) {
-          this.danger("The DID must begin with 'did:'", "Invalid DID");
-          return;
-        }
-      } catch (e) {
-        logger.error("Error parsing QR info:", e);
-        this.danger("Could not parse the QR info.", "Read Error");
+    if (!url) {
+      this.danger("No QR code detected. Please try again.", "Scan Error");
+      return;
+    }
+
+    // Validate URL format first
+    if (!url.startsWith("http://") && !url.startsWith("https://")) {
+      this.danger(
+        "Invalid QR code format. Please scan a valid TimeSafari contact QR code.",
+        "Invalid Format",
+      );
+      return;
+    }
+
+    let newContact: Contact;
+    try {
+      // Extract JWT from URL
+      const jwt = getContactJwtFromJwtUrl(url);
+      if (!jwt) {
+        this.danger(
+          "Could not extract contact information from the QR code. Please try again.",
+          "Invalid QR Code",
+        );
         return;
       }
 
-      try {
-        await db.open();
-        await db.contacts.add(newContact);
+      // Validate JWT format
+      if (
+        !jwt.match(/^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/)
+      ) {
+        this.danger(
+          "The QR code contains invalid data. Please scan a valid TimeSafari contact QR code.",
+          "Invalid Data",
+        );
+        return;
+      }
 
-        let addedMessage;
-        if (this.activeDid) {
-          await this.setVisibility(newContact, true);
-          newContact.seesMe = true; // didn't work inside setVisibility
-          addedMessage =
-            "They were added, and your activity is visible to them.";
-        } else {
-          addedMessage = "They were added.";
-        }
-        this.$notify(
-          {
-            group: "alert",
-            type: "success",
-            title: "Contact Added",
-            text: addedMessage,
-          },
-          3000,
+      const { payload } = decodeEndorserJwt(jwt);
+      if (!payload) {
+        this.danger(
+          "Could not decode the contact information. Please try again.",
+          "Decode Error",
         );
+        return;
+      }
 
-        if (this.isRegistered) {
-          if (!this.hideRegisterPromptOnNewContact && !newContact.registered) {
-            setTimeout(() => {
-              this.$notify(
-                {
-                  group: "modal",
-                  type: "confirm",
-                  title: "Register",
-                  text: "Do you want to register them?",
-                  onCancel: async (stopAsking: boolean) => {
-                    if (stopAsking) {
-                      await db.settings.update(MASTER_SETTINGS_KEY, {
-                        hideRegisterPromptOnNewContact: stopAsking,
-                      });
-                      this.hideRegisterPromptOnNewContact = stopAsking;
-                    }
-                  },
-                  onNo: async (stopAsking: boolean) => {
-                    if (stopAsking) {
-                      await db.settings.update(MASTER_SETTINGS_KEY, {
-                        hideRegisterPromptOnNewContact: stopAsking,
-                      });
-                      this.hideRegisterPromptOnNewContact = stopAsking;
-                    }
-                  },
-                  onYes: async () => {
-                    await this.register(newContact);
-                  },
-                  promptToStopAsking: true,
-                },
-                -1,
-              );
-            }, 500);
-          }
-        }
-      } catch (e) {
-        logger.error("Error saving contact info:", e);
-        this.$notify(
-          {
-            group: "alert",
-            type: "danger",
-            title: "Contact Error",
-            text: "Could not save contact info. Check if it already exists.",
-          },
-          5000,
+      // Validate required fields
+      if (!payload.own && !payload.iss) {
+        this.danger(
+          "Missing required contact information. Please scan a valid TimeSafari contact QR code.",
+          "Incomplete Data",
         );
+        return;
       }
-    } else {
+
+      newContact = {
+        did: payload.own?.did || payload.iss,
+        name: payload.own?.name,
+        nextPubKeyHashB64: payload.own?.nextPublicEncKeyHash,
+        profileImageUrl: payload.own?.profileImageUrl,
+        publicKeyBase64: payload.own?.publicEncKey,
+        registered: payload.own?.registered,
+      };
+
+      if (!newContact.did) {
+        this.danger(
+          "Missing contact identifier. Please scan a valid TimeSafari contact QR code.",
+          "Incomplete Contact",
+        );
+        return;
+      }
+
+      if (!isDid(newContact.did)) {
+        this.danger(
+          "Invalid contact identifier format. The identifier must begin with 'did:'.",
+          "Invalid Identifier",
+        );
+        return;
+      }
+
+      await db.open();
+      await db.contacts.add(newContact);
+
+      let addedMessage;
+      if (this.activeDid) {
+        await this.setVisibility(newContact, true);
+        newContact.seesMe = true;
+        addedMessage = "They were added, and your activity is visible to them.";
+      } else {
+        addedMessage = "They were added.";
+      }
+
       this.$notify(
         {
           group: "alert",
-          type: "danger",
-          title: "Invalid Contact QR Code",
-          text: "No QR code detected with contact information.",
+          type: "success",
+          title: "Contact Added",
+          text: addedMessage,
         },
-        5000,
+        3000,
+      );
+
+      if (
+        this.isRegistered &&
+        !this.hideRegisterPromptOnNewContact &&
+        !newContact.registered
+      ) {
+        setTimeout(() => {
+          this.$notify(
+            {
+              group: "modal",
+              type: "confirm",
+              title: "Register",
+              text: "Do you want to register them?",
+              onCancel: async (stopAsking?: boolean) => {
+                if (stopAsking) {
+                  await db.settings.update(MASTER_SETTINGS_KEY, {
+                    hideRegisterPromptOnNewContact: stopAsking,
+                  });
+                  this.hideRegisterPromptOnNewContact = stopAsking;
+                }
+              },
+              onNo: async (stopAsking?: boolean) => {
+                if (stopAsking) {
+                  await db.settings.update(MASTER_SETTINGS_KEY, {
+                    hideRegisterPromptOnNewContact: stopAsking,
+                  });
+                  this.hideRegisterPromptOnNewContact = stopAsking;
+                }
+              },
+              onYes: async () => {
+                await this.register(newContact);
+              },
+              promptToStopAsking: true,
+            },
+            -1,
+          );
+        }, 500);
+      }
+    } catch (e) {
+      logger.error("Error processing QR code:", e);
+      this.danger(
+        "Could not process the QR code. Please make sure you're scanning a valid TimeSafari contact QR code.",
+        "Processing Error",
       );
     }
   }
 
-  async setVisibility(contact: Contact, visibility: boolean) {
+  async setVisibility(contact: Contact, visibility: boolean): Promise<void> {
     const result = await setVisibilityUtil(
       this.activeDid,
       this.apiServer,
@@ -314,7 +497,7 @@ export default class ContactQRScanShow extends Vue {
     }
   }
 
-  async register(contact: Contact) {
+  async register(contact: Contact): Promise<void> {
     this.$notify(
       {
         group: "alert",
@@ -364,17 +547,19 @@ export default class ContactQRScanShow extends Vue {
       let userMessage = "There was an error.";
       const serverError = error as AxiosError;
       if (serverError) {
-        if (serverError.response?.data?.error?.message) {
-          userMessage = serverError.response.data.error.message;
+        const responseData = serverError.response?.data as {
+          error?: { message?: string };
+        };
+        if (responseData?.error?.message) {
+          userMessage = responseData.error.message;
         } else if (serverError.message) {
-          userMessage = serverError.message; // Info for the user
+          userMessage = serverError.message;
         } else {
           userMessage = JSON.stringify(serverError.toJSON());
         }
       } else {
         userMessage = error as string;
       }
-      // Now set that error for the user to see.
       this.$notify(
         {
           group: "alert",
@@ -388,7 +573,7 @@ export default class ContactQRScanShow extends Vue {
   }
 
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  onScanError(error: any) {
+  onScanError(error: any): void {
     logger.error("Scan was invalid:", error);
     this.$notify(
       {
@@ -401,39 +586,131 @@ export default class ContactQRScanShow extends Vue {
     );
   }
 
-  onCopyUrlToClipboard() {
-    //this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing
-    useClipboard()
-      .copy(this.qrValue)
-      .then(() => {
-        // console.log("Contact URL:", this.qrValue);
-        this.$notify(
-          {
-            group: "alert",
-            type: "toast",
-            title: "Copied",
-            text: "Contact URL was copied to clipboard.",
-          },
-          2000,
-        );
+  async onCopyUrlToClipboard(): Promise<void> {
+    try {
+      await this.platformService.writeToClipboard(this.qrValue);
+      this.$notify(
+        {
+          group: "alert",
+          type: "toast",
+          title: "Copied",
+          text: "Contact URL was copied to clipboard.",
+        },
+        2000,
+      );
+    } catch (error) {
+      logger.error("Error copying to clipboard:", error);
+      this.danger("Failed to copy to clipboard", "Error");
+    }
+  }
+
+  async onCopyDidToClipboard(): Promise<void> {
+    try {
+      await this.platformService.writeToClipboard(this.activeDid);
+      this.$notify(
+        {
+          group: "alert",
+          type: "info",
+          title: "Copied",
+          text: "Your DID was copied to the clipboard. Have them paste it in the box on their 'People' screen to add you.",
+        },
+        5000,
+      );
+    } catch (error) {
+      logger.error("Error copying to clipboard:", error);
+      this.danger("Failed to copy to clipboard", "Error");
+    }
+  }
+
+  async copyToClipboard(text: string): Promise<void> {
+    try {
+      await this.platformService.writeToClipboard(text);
+      this.$notify(
+        {
+          group: "alert",
+          type: "success",
+          title: "Copied to clipboard",
+          text: "The DID has been copied to your clipboard.",
+        },
+        3000,
+      );
+    } catch (error) {
+      this.$notify(
+        {
+          group: "alert",
+          type: "danger",
+          title: "Error",
+          text: "Failed to copy to clipboard.",
+        },
+        3000,
+      );
+    }
+  }
+
+  async requestCameraPermission(): Promise<void> {
+    try {
+      const capabilities = this.platformService.getCapabilities();
+      if (capabilities.hasCamera) {
+        try {
+          await this.platformService.takePicture();
+          this.$notify(
+            {
+              group: "alert",
+              type: "success",
+              title: "Camera Access Granted",
+              text: "You can now scan QR codes.",
+            },
+            3000,
+          );
+        } catch (error) {
+          this.$notify(
+            {
+              group: "alert",
+              type: "danger",
+              title: "Camera Access Denied",
+              text: "Please enable camera access in your device settings.",
+            },
+            5000,
+          );
+        }
+      }
+    } catch (error) {
+      this.$notify(
+        {
+          group: "alert",
+          type: "danger",
+          title: "Error",
+          text: "Failed to request camera permission.",
+        },
+        3000,
+      );
+    }
+  }
+
+  async onCancel(stopAsking?: boolean) {
+    if (stopAsking) {
+      await db.settings.update(MASTER_SETTINGS_KEY, {
+        hideRegisterPromptOnNewContact: stopAsking,
       });
+      this.hideRegisterPromptOnNewContact = stopAsking;
+    }
   }
 
-  onCopyDidToClipboard() {
-    //this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing
-    useClipboard()
-      .copy(this.activeDid)
-      .then(() => {
-        this.$notify(
-          {
-            group: "alert",
-            type: "info",
-            title: "Copied",
-            text: "Your DID was copied to the clipboard. Have them paste it in the box on their 'People' screen to add you.",
-          },
-          5000,
-        );
+  async onNo(stopAsking?: boolean) {
+    if (stopAsking) {
+      await db.settings.update(MASTER_SETTINGS_KEY, {
+        hideRegisterPromptOnNewContact: stopAsking,
       });
+      this.hideRegisterPromptOnNewContact = stopAsking;
+    }
+  }
+
+  get useQRReader(): boolean {
+    return __USE_QR_READER__;
+  }
+
+  get isMobile(): boolean {
+    return __IS_MOBILE__;
   }
 }
 </script>
diff --git a/test-scripts/generate_test_qr.js b/test-scripts/generate_test_qr.js
new file mode 100644
index 00000000..40cae750
--- /dev/null
+++ b/test-scripts/generate_test_qr.js
@@ -0,0 +1,44 @@
+const { createJWT, SimpleSigner } = require('did-jwt');
+const { Buffer } = require('buffer');
+const qrcode = require('qrcode');
+const fs = require('fs');
+
+// Test account details
+const testAccount = {
+  did: 'did:ethr:0x1234567890123456789012345678901234567890',
+  privateKey: '0x1234567890123456789012345678901234567890123456789012345678901234',
+  publicKey: '0x12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678'
+};
+
+async function generateTestQR() {
+  // Create contact info payload
+  const contactInfo = {
+    iat: Date.now(),
+    iss: testAccount.did,
+    own: {
+      did: testAccount.did,
+      name: 'Test User',
+      publicEncKey: Buffer.from(testAccount.publicKey.slice(2), 'hex').toString('base64'),
+      registered: true,
+      profileImageUrl: 'https://example.com/profile.jpg'
+    }
+  };
+
+  // Create JWT
+  const signer = await SimpleSigner(testAccount.privateKey);
+  const jwt = await createJWT(contactInfo, {
+    issuer: testAccount.did,
+    signer
+  });
+
+  // Create QR code URL
+  const url = `https://timesafari.app/contact/confirm/${jwt}`;
+
+  // Generate QR code and save as PNG
+  const qrCode = await qrcode.toBuffer(url);
+  fs.writeFileSync('test_qr.png', qrCode);
+  console.log('QR Code URL:', url);
+  console.log('QR code saved as test_qr.png');
+}
+
+generateTestQR().catch(console.error); 
\ No newline at end of file
diff --git a/test-scripts/package-lock.json b/test-scripts/package-lock.json
index 64a66cea..ec7ebd80 100644
--- a/test-scripts/package-lock.json
+++ b/test-scripts/package-lock.json
@@ -12,7 +12,8 @@
         "@ethersproject/hdnode": "^5.7.0",
         "@ethersproject/wallet": "^5.7.0",
         "axios": "^1.6.2",
-        "did-jwt": "^6.11.6"
+        "did-jwt": "^6.11.6",
+        "qrcode": "^1.5.4"
       },
       "devDependencies": {
         "@types/node": "^20.10.0",
@@ -860,6 +861,30 @@
       "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==",
       "license": "MIT"
     },
+    "node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
     "node_modules/arg": {
       "version": "4.1.3",
       "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
@@ -915,12 +940,50 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/camelcase": {
+      "version": "5.3.1",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/canonicalize": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/canonicalize/-/canonicalize-2.0.0.tgz",
       "integrity": "sha512-ulDEYPv7asdKvqahuAY35c1selLdzDwHqugK92hfkzvlDCwXRRelDkR+Er33md/PtnpqHemgkuDPanZ4fiYZ8w==",
       "license": "Apache-2.0"
     },
+    "node_modules/cliui": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+      "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.0",
+        "wrap-ansi": "^6.2.0"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "license": "MIT"
+    },
     "node_modules/combined-stream": {
       "version": "1.0.8",
       "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -940,6 +1003,15 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/decamelize": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/delayed-stream": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -985,6 +1057,12 @@
         "node": ">=0.3.1"
       }
     },
+    "node_modules/dijkstrajs": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
+      "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
+      "license": "MIT"
+    },
     "node_modules/dunder-proto": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -1020,6 +1098,12 @@
       "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==",
       "license": "MIT"
     },
+    "node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "license": "MIT"
+    },
     "node_modules/es-define-property": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -1065,6 +1149,19 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/find-up": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+      "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+      "license": "MIT",
+      "dependencies": {
+        "locate-path": "^5.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/follow-redirects": {
       "version": "1.15.9",
       "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
@@ -1109,6 +1206,15 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/get-caller-file": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+      "license": "ISC",
+      "engines": {
+        "node": "6.* || 8.* || >= 10.*"
+      }
+    },
     "node_modules/get-intrinsic": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -1224,12 +1330,33 @@
       "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
       "license": "ISC"
     },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/js-sha3": {
       "version": "0.8.0",
       "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
       "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==",
       "license": "MIT"
     },
+    "node_modules/locate-path": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+      "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+      "license": "MIT",
+      "dependencies": {
+        "p-locate": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/make-error": {
       "version": "1.3.6",
       "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
@@ -1285,18 +1412,136 @@
       "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==",
       "license": "(Apache-2.0 AND MIT)"
     },
+    "node_modules/p-limit": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+      "license": "MIT",
+      "dependencies": {
+        "p-try": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-locate": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+      "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+      "license": "MIT",
+      "dependencies": {
+        "p-limit": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/p-try": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/pngjs": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
+      "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
     "node_modules/proxy-from-env": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
       "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
       "license": "MIT"
     },
+    "node_modules/qrcode": {
+      "version": "1.5.4",
+      "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
+      "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
+      "license": "MIT",
+      "dependencies": {
+        "dijkstrajs": "^1.0.1",
+        "pngjs": "^5.0.0",
+        "yargs": "^15.3.1"
+      },
+      "bin": {
+        "qrcode": "bin/qrcode"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/require-main-filename": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+      "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+      "license": "ISC"
+    },
     "node_modules/scrypt-js": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz",
       "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==",
       "license": "MIT"
     },
+    "node_modules/set-blocking": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+      "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+      "license": "ISC"
+    },
+    "node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/ts-node": {
       "version": "10.9.2",
       "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
@@ -1378,6 +1623,67 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/which-module": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
+      "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
+      "license": "ISC"
+    },
+    "node_modules/wrap-ansi": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+      "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/y18n": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+      "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
+      "license": "ISC"
+    },
+    "node_modules/yargs": {
+      "version": "15.4.1",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+      "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+      "license": "MIT",
+      "dependencies": {
+        "cliui": "^6.0.0",
+        "decamelize": "^1.2.0",
+        "find-up": "^4.1.0",
+        "get-caller-file": "^2.0.1",
+        "require-directory": "^2.1.1",
+        "require-main-filename": "^2.0.0",
+        "set-blocking": "^2.0.0",
+        "string-width": "^4.2.0",
+        "which-module": "^2.0.0",
+        "y18n": "^4.0.0",
+        "yargs-parser": "^18.1.2"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/yargs-parser": {
+      "version": "18.1.3",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+      "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+      "license": "ISC",
+      "dependencies": {
+        "camelcase": "^5.0.0",
+        "decamelize": "^1.2.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/yn": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
diff --git a/test-scripts/package.json b/test-scripts/package.json
index fd57f96d..7ed6434f 100644
--- a/test-scripts/package.json
+++ b/test-scripts/package.json
@@ -13,11 +13,12 @@
     "@ethersproject/hdnode": "^5.7.0",
     "@ethersproject/wallet": "^5.7.0",
     "axios": "^1.6.2",
-    "did-jwt": "^6.11.6"
+    "did-jwt": "^6.11.6",
+    "qrcode": "^1.5.4"
   },
   "devDependencies": {
     "@types/node": "^20.10.0",
     "ts-node": "^10.9.1",
     "typescript": "^5.3.2"
   }
-} 
\ No newline at end of file
+}
diff --git a/test_qr.png b/test_qr.png
new file mode 100644
index 00000000..47e51824
Binary files /dev/null and b/test_qr.png differ
diff --git a/vite.config.ts b/vite.config.ts
index c3702bc8..4fbd782c 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,46 +1,59 @@
 import { defineConfig } from "vite";
 import vue from "@vitejs/plugin-vue";
 import path from "path";
+import { fileURLToPath, URL } from 'node:url';
 
-export default defineConfig({
-  plugins: [vue()],
-  resolve: {
-    alias: {
-      '@': path.resolve(__dirname, './src'),
-      'nostr-tools': path.resolve(__dirname, 'node_modules/nostr-tools'),
-      'nostr-tools/nip06': path.resolve(__dirname, 'node_modules/nostr-tools/nip06'),
-      'nostr-tools/core': path.resolve(__dirname, 'node_modules/nostr-tools/core'),
-      stream: 'stream-browserify',
-      util: 'util',
-      crypto: 'crypto-browserify'
+// Determine if we're building for mobile based on environment variable
+const isMobile = process.env.VITE_PLATFORM === 'capacitor';
+
+export default defineConfig(({ mode }) => {
+  return {
+    plugins: [vue()],
+    resolve: {
+      alias: {
+        '@': fileURLToPath(new URL('./src', import.meta.url)),
+        'nostr-tools': path.resolve(__dirname, 'node_modules/nostr-tools'),
+        'nostr-tools/nip06': path.resolve(__dirname, 'node_modules/nostr-tools/nip06'),
+        'nostr-tools/core': path.resolve(__dirname, 'node_modules/nostr-tools/core'),
+        stream: 'stream-browserify',
+        util: 'util',
+        crypto: 'crypto-browserify'
+      },
+      mainFields: ['module', 'jsnext:main', 'jsnext', 'main'],
     },
-    mainFields: ['module', 'jsnext:main', 'jsnext', 'main'],
-  },
-  optimizeDeps: {
-    include: ['nostr-tools', 'nostr-tools/nip06', 'nostr-tools/core'],
-    esbuildOptions: {
-      define: {
-        global: 'globalThis'
+    optimizeDeps: {
+      include: ['nostr-tools', 'nostr-tools/nip06', 'nostr-tools/core'],
+      esbuildOptions: {
+        define: {
+          global: 'globalThis'
+        }
       }
-    }
-  },
-  build: {
-    sourcemap: true,
-    target: 'esnext',
-    chunkSizeWarningLimit: 1000,
-    commonjsOptions: {
-      include: [/node_modules/],
-      transformMixedEsModules: true
     },
-    rollupOptions: {
-      external: ['stream', 'util', 'crypto'],
-      output: {
-        globals: {
-          stream: 'stream',
-          util: 'util',
-          crypto: 'crypto'
+    define: {
+      __USE_QR_READER__: JSON.stringify(!isMobile),
+      __IS_MOBILE__: JSON.stringify(isMobile),
+    },
+    build: {
+      sourcemap: true,
+      target: 'esnext',
+      chunkSizeWarningLimit: 1000,
+      commonjsOptions: {
+        include: [/node_modules/],
+        transformMixedEsModules: true
+      },
+      rollupOptions: {
+        external: isMobile ? ['vue-qrcode-reader'] : [],
+        output: {
+          globals: {
+            stream: 'stream',
+            util: 'util',
+            crypto: 'crypto'
+          },
+          entryFileNames: `[name]${isMobile ? '-mobile' : ''}.js`,
+          chunkFileNames: `[name]${isMobile ? '-mobile' : ''}.js`,
+          assetFileNames: `[name]${isMobile ? '-mobile' : ''}.[ext]`
         }
       }
     }
-  }
+  };
 }); 
\ No newline at end of file