Browse Source

De-coupled web and mobile QR scanner views

- Separate scanner views for web and mobile platforms: different libraries, similar layouts
- Mobile: QR code overlaid on top of full-screen camera view
- Mobile: added framing box + instruction text
- Mobile: increased debounce time to compensate for behavior of MLkit scanner
- Web: removed Capacitor-related code and platform-specific conditions
- Web: adjusted max-size of QR code and camera view to better fit newer iOS device screens
- Web + mobile: camera view remains active when a QR scan is triggered
qrcode-reboot
Jose Olarte III 2 days ago
parent
commit
ce34257ba1
  1. 2
      capacitor.config.json
  2. 42
      ios/App/App.xcodeproj/project.pbxproj
  3. 189
      src/views/ContactQRScanFullView.vue
  4. 40
      src/views/ContactQRScanShowView.vue
  5. 15
      src/views/ContactsView.vue

2
capacitor.config.json

@ -1,5 +1,5 @@
{ {
"appId": "app.timesafari", "appId": "app.trentlarson.timesafari",
"appName": "TimeSafari", "appName": "TimeSafari",
"webDir": "dist", "webDir": "dist",
"bundledWebRuntime": false, "bundledWebRuntime": false,

42
ios/App/App.xcodeproj/project.pbxproj

@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 48; objectVersion = 54;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@ -123,8 +123,9 @@
504EC2FC1FED79650016851F /* Project object */ = { 504EC2FC1FED79650016851F /* Project object */ = {
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 920; LastSwiftUpdateCheck = 920;
LastUpgradeCheck = 920; LastUpgradeCheck = 1630;
TargetAttributes = { TargetAttributes = {
504EC3031FED79650016851F = { 504EC3031FED79650016851F = {
CreatedOnToolsVersion = 9.2; CreatedOnToolsVersion = 9.2;
@ -268,6 +269,7 @@
CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES; CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_EMPTY_BODY = YES;
@ -275,8 +277,10 @@
CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES;
@ -286,8 +290,10 @@
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 7XVXYPEQYJ;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO; GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES; GCC_NO_COMMON_BLOCKS = YES;
@ -325,6 +331,7 @@
CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES; CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_EMPTY_BODY = YES;
@ -332,8 +339,10 @@
CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES;
@ -343,8 +352,10 @@
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 7XVXYPEQYJ;
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES; GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@ -356,7 +367,8 @@
IPHONEOS_DEPLOYMENT_TARGET = 13.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES; VALIDATE_PRODUCT = YES;
}; };
name = Release; name = Release;
@ -368,18 +380,22 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 18; CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = GM3FS5JQPH; ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.4.7; MARKETING_VERSION = 0.4.7;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari; PRODUCT_BUNDLE_IDENTIFIER = app.trentlarson.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic"; /* allows agvtool to set *_VERSION settings, but new-marketing-version sets MARKETING_VERSION in Info.plist instead */ VERSIONING_SYSTEM = "apple-generic";
}; };
name = Debug; name = Debug;
}; };
@ -390,17 +406,21 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 18; CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = GM3FS5JQPH; ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.4.7; MARKETING_VERSION = 0.4.7;
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari; PRODUCT_BUNDLE_IDENTIFIER = app.trentlarson.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic"; /* allows agvtool to set *_VERSION settings, but new-marketing-version sets MARKETING_VERSION in Info.plist instead */ VERSIONING_SYSTEM = "apple-generic";
}; };
name = Release; name = Release;
}; };

189
src/views/ContactQRScanFullView.vue

@ -1,24 +1,104 @@
<template> <template>
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="relativew-[100vw] h-[100vh]"> <section id="Content" class="relative w-[100vw] h-[100vh]">
<div <div
class="absolute inset-x-0 bottom-0 bg-black/50 p-6 pb-[calc(env(safe-area-inset-bottom)+1.5rem)]" class="p-6 bg-white w-full max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto"
> >
<p class="text-center text-white mb-3"> <div class="mb-4">
Point your camera at a TimeSafari contact QR code to scan it <h1 class="text-xl text-center font-semibold relative">
automatically. <!-- Back -->
</p> <a
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="handleBack"
>
<font-awesome icon="chevron-left" class="fa-fw" />
</a>
<!-- Quick Help -->
<a
class="text-xl text-center text-blue-500 px-2 py-1 absolute -right-2 -top-1"
@click="toastQRCodeHelp()"
>
<font-awesome icon="circle-question" class="fa-fw" />
</a>
<p v-if="error" class="text-center text-rose-300 mb-3">{{ error }}</p> Share Contact Info
</h1>
</div>
<div class="flex justify-center items-center"> <div
v-if="!givenName"
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mt-4"
>
<p class="mb-2">
<b>Note:</b> your identity currently does <b>not</b> include a name.
</p>
<button <button
class="text-center text-slate-600 leading-none bg-white p-2 rounded-full drop-shadow-lg" class="inline-block text-md uppercase 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"
@click="handleBack" @click="openUserNameDialog"
> >
<font-awesome icon="xmark" class="size-6"></font-awesome> Set Your Name
</button> </button>
</div> </div>
<UserNameDialog ref="userNameDialog" />
<div
v-if="activeDid && activeDid.startsWith(ETHR_DID_PREFIX)"
class="block w-full max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto mt-4"
@click="onCopyUrlToClipboard()"
>
<!--
Play with display options: https://qr-code-styling.com/
See docs: https://www.npmjs.com/package/qr-code-generator-vue3
-->
<QRCodeVue3
:value="qrValue"
:width="606"
:height="606"
:corners-square-options="{ type: 'square' }"
:dots-options="{ type: 'square', color: '#000' }"
/>
</div>
<div v-else-if="activeDid" class="text-center mt-4">
<!-- Not an ETHR DID so force them to paste it. (Passkey Peer DIDs are too big.) -->
<span class="text-blue-500" @click="onCopyDidToClipboard()">
Click here to copy your DID to your clipboard.
</span>
<span>
Then give it to them so they can paste it in their list of People.
</span>
</div>
<div v-else class="text-center mt-4">
You have no identitifiers yet, so
<router-link
:to="{ name: 'start' }"
class="bg-blue-500 text-white px-1.5 py-1 rounded-md"
>
create your identifier.
</router-link>
<br />
If you don't do that first, these contacts won't see your activity.
</div>
</div>
<div
class="relative w-full max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto border border-dashed border-white mt-8 aspect-square"
>
<p
class="absolute top-0 left-0 right-0 bg-black bg-opacity-50 text-white text-sm text-center py-2 z-10"
>
Position QR code in the frame
</p>
<p
v-if="error"
class="absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 text-sm text-center py-2 z-20 text-rose-400"
>
{{ error }}
</p>
</div> </div>
</section> </section>
</template> </template>
@ -33,18 +113,29 @@ import { NotificationIface } from "../constants/app";
import { db } from "../db/index"; import { db } from "../db/index";
import { Contact } from "../db/tables/contacts"; import { Contact } from "../db/tables/contacts";
import { getContactJwtFromJwtUrl } from "../libs/crypto"; import { getContactJwtFromJwtUrl } from "../libs/crypto";
import { decodeEndorserJwt } from "../libs/crypto/vc"; import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
import { retrieveSettingsForActiveAccount } from "../db/index"; import { retrieveSettingsForActiveAccount } from "../db/index";
import { setVisibilityUtil } from "../libs/endorserServer"; import { setVisibilityUtil } from "../libs/endorserServer";
import { useClipboard } from "@vueuse/core";
import QRCodeVue3 from "qr-code-generator-vue3";
import UserNameDialog from "../components/UserNameDialog.vue";
import { generateEndorserJwtUrlForAccount } from "../libs/endorserServer";
import { retrieveAccountMetadata } from "../libs/util";
interface QRScanResult { interface QRScanResult {
rawValue?: string; rawValue?: string;
barcode?: string; barcode?: string;
} }
interface IUserNameDialog {
open: (callback: (name: string) => void) => void;
}
@Component({ @Component({
components: { components: {
QuickNav, QuickNav,
QRCodeVue3,
UserNameDialog,
}, },
}) })
export default class ContactQRScan extends Vue { export default class ContactQRScan extends Vue {
@ -55,11 +146,14 @@ export default class ContactQRScan extends Vue {
error: string | null = null; error: string | null = null;
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
givenName = "";
qrValue = "";
ETHR_DID_PREFIX = ETHR_DID_PREFIX;
// Add new properties to track scanning state // Add new properties to track scanning state
private lastScannedValue: string = ""; private lastScannedValue: string = "";
private lastScanTime: number = 0; private lastScanTime: number = 0;
private readonly SCAN_DEBOUNCE_MS = 2000; // Prevent duplicate scans within 2 seconds private readonly SCAN_DEBOUNCE_MS = 5000; // Increased from 2000 to 5000ms to better handle mobile scanning
// Add cleanup tracking // Add cleanup tracking
private isCleaningUp = false; private isCleaningUp = false;
@ -70,6 +164,21 @@ export default class ContactQRScan extends Vue {
const settings = await retrieveSettingsForActiveAccount(); const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || ""; this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || ""; this.apiServer = settings.apiServer || "";
this.givenName = settings.firstName || "";
const account = await retrieveAccountMetadata(this.activeDid);
if (account) {
const name =
(settings.firstName || "") +
(settings.lastName ? ` ${settings.lastName}` : "");
this.qrValue = await generateEndorserJwtUrlForAccount(
account,
!!settings.isRegistered,
name,
settings.profileImageUrl || "",
false,
);
}
} catch (error) { } catch (error) {
logger.error("Error initializing component:", { logger.error("Error initializing component:", {
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),
@ -270,14 +379,12 @@ export default class ContactQRScan extends Vue {
notes: contactInfo.notes || "", notes: contactInfo.notes || "",
}; };
// Add contact and stop scanning // Add contact but keep scanning
logger.info("Adding new contact to database:", { logger.info("Adding new contact to database:", {
did: contact.did, did: contact.did,
name: contact.name, name: contact.name,
}); });
await this.addNewContact(contact); await this.addNewContact(contact);
await this.stopScanning();
this.$router.back(); // Return to previous view after successful scan
} catch (error) { } catch (error) {
logger.error("Error processing contact QR code:", { logger.error("Error processing contact QR code:", {
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),
@ -420,6 +527,56 @@ export default class ContactQRScan extends Vue {
await this.cleanupScanner(); await this.cleanupScanner();
this.$router.back(); this.$router.back();
} }
toastQRCodeHelp() {
this.$notify(
{
group: "alert",
type: "info",
title: "QR Code Help",
text: "Click the QR code to copy your contact info to your clipboard.",
},
5000,
);
}
onCopyUrlToClipboard() {
useClipboard()
.copy(this.qrValue)
.then(() => {
this.$notify(
{
group: "alert",
type: "toast",
title: "Copied",
text: "Contact URL was copied to clipboard.",
},
2000,
);
});
}
onCopyDidToClipboard() {
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,
);
});
}
openUserNameDialog() {
(this.$refs.userNameDialog as IUserNameDialog).open((name: string) => {
this.givenName = name;
});
}
} }
</script> </script>

40
src/views/ContactQRScanShowView.vue

@ -42,7 +42,7 @@
<div <div
v-if="activeDid && activeDid.startsWith(ETHR_DID_PREFIX)" v-if="activeDid && activeDid.startsWith(ETHR_DID_PREFIX)"
class="block w-[90vw] max-w-[40vh] mx-auto my-4" class="block w-[90vw] max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto my-4"
@click="onCopyUrlToClipboard()" @click="onCopyUrlToClipboard()"
> >
<!-- <!--
@ -75,13 +75,13 @@
create your identifier. create your identifier.
</router-link> </router-link>
<br /> <br />
If you don't that first, these contacts won't see your activity. If you don't do that first, these contacts won't see your activity.
</div> </div>
<div class="text-center mt-6"> <div class="text-center mt-6">
<div <div
v-if="isScanning && !isNativePlatform" v-if="isScanning"
class="relative aspect-square overflow-hidden bg-slate-800 w-[90vw] max-w-[40vh] mx-auto" class="relative aspect-square overflow-hidden bg-slate-800 w-[90vw] max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto"
> >
<!-- Status Message --> <!-- Status Message -->
<div <div
@ -142,7 +142,7 @@
</div> </div>
<qrcode-stream <qrcode-stream
v-if="useQRReader && !isNativePlatform" v-if="useQRReader"
:camera="preferredCamera" :camera="preferredCamera"
@decode="onDecode" @decode="onDecode"
@init="onInit" @init="onInit"
@ -167,17 +167,9 @@
</div> </div>
<div <div
v-else v-else
class="flex items-center justify-center aspect-square overflow-hidden bg-slate-800 w-[90vw] max-w-[40vh] mx-auto" class="flex items-center justify-center aspect-square overflow-hidden bg-slate-800 w-[90vw] max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto"
> >
<button <button
v-if="isNativePlatform"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white text-lg px-3 py-2 rounded-lg"
@click="$router.push({ name: 'contact-qr-scan-full' })"
>
Scan QR Code
</button>
<button
v-else
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white text-lg px-3 py-2 rounded-lg" class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white text-lg px-3 py-2 rounded-lg"
@click="startScanning" @click="startScanning"
> >
@ -193,7 +185,6 @@ import { AxiosError } from "axios";
import QRCodeVue3 from "qr-code-generator-vue3"; import QRCodeVue3 from "qr-code-generator-vue3";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import { Capacitor } from "@capacitor/core";
import { QrcodeStream } from "vue-qrcode-reader"; import { QrcodeStream } from "vue-qrcode-reader";
import QuickNav from "../components/QuickNav.vue"; import QuickNav from "../components/QuickNav.vue";
@ -244,7 +235,6 @@ export default class ContactQRScanShow extends Vue {
qrValue = ""; qrValue = "";
isScanning = false; isScanning = false;
error: string | null = null; error: string | null = null;
isNativePlatform = Capacitor.isNativePlatform();
// QR Scanner properties // QR Scanner properties
isInitializing = true; isInitializing = true;
@ -534,13 +524,12 @@ export default class ContactQRScanShow extends Vue {
notes: contactInfo.notes || "", notes: contactInfo.notes || "",
}; };
// Add contact and stop scanning // Add contact but keep scanning
logger.info("Adding new contact to database:", { logger.info("Adding new contact to database:", {
did: contact.did, did: contact.did,
name: contact.name, name: contact.name,
}); });
await this.addNewContact(contact); await this.addNewContact(contact);
await this.stopScanning();
} catch (error) { } catch (error) {
logger.error("Error processing contact QR code:", { logger.error("Error processing contact QR code:", {
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),
@ -692,6 +681,7 @@ export default class ContactQRScanShow extends Vue {
{ {
group: "alert", group: "alert",
type: "info", type: "info",
title: "QR Code Help",
text: "Click the QR code to copy your contact info to your clipboard.", text: "Click the QR code to copy your contact info to your clipboard.",
}, },
5000, 5000,
@ -726,11 +716,9 @@ export default class ContactQRScanShow extends Vue {
this.isMounted = true; this.isMounted = true;
document.addEventListener("pause", this.handleAppPause); document.addEventListener("pause", this.handleAppPause);
document.addEventListener("resume", this.handleAppResume); document.addEventListener("resume", this.handleAppResume);
// Start scanning automatically when view is loaded, but only on web platform // Start scanning automatically when view is loaded
if (!this.isNativePlatform) {
this.startScanning(); this.startScanning();
} }
}
beforeDestroy() { beforeDestroy() {
this.isMounted = false; this.isMounted = false;
@ -856,11 +844,6 @@ export default class ContactQRScanShow extends Vue {
async onInit(promise: Promise<void>): Promise<void> { async onInit(promise: Promise<void>): Promise<void> {
logger.log("[QRScanner] onInit called"); logger.log("[QRScanner] onInit called");
if (this.isNativePlatform) {
logger.log("Skipping QR scanner initialization on native platform");
return;
}
try { try {
await promise; await promise;
this.isInitializing = false; this.isInitializing = false;
@ -889,7 +872,7 @@ export default class ContactQRScanShow extends Vue {
onDetect(result: unknown): void { onDetect(result: unknown): void {
this.isScanning = true; this.isScanning = true;
this.cameraState = "detecting"; this.cameraState = "active";
try { try {
let rawValue: string | undefined; let rawValue: string | undefined;
if ( if (
@ -899,7 +882,7 @@ export default class ContactQRScanShow extends Vue {
) { ) {
rawValue = result[0].rawValue; rawValue = result[0].rawValue;
} else if (result && typeof result === "object" && "rawValue" in result) { } else if (result && typeof result === "object" && "rawValue" in result) {
rawValue = result.rawValue; rawValue = (result as { rawValue: string }).rawValue;
} }
if (rawValue) { if (rawValue) {
this.isInitializing = false; this.isInitializing = false;
@ -909,7 +892,6 @@ export default class ContactQRScanShow extends Vue {
} catch (error) { } catch (error) {
this.handleError(error); this.handleError(error);
} finally { } finally {
this.isScanning = false;
this.cameraState = "active"; this.cameraState = "active";
} }
} }

15
src/views/ContactsView.vue

@ -69,12 +69,12 @@
</span> </span>
</span> </span>
<router-link <button
:to="{ name: 'contact-qr' }"
class="flex items-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md" class="flex items-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
@click="handleQRCodeClick"
> >
<font-awesome icon="qrcode" class="fa-fw text-2xl" /> <font-awesome icon="qrcode" class="fa-fw text-2xl" />
</router-link> </button>
<textarea <textarea
v-model="contactInput" v-model="contactInput"
@ -353,6 +353,7 @@ import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { RouteLocationNormalizedLoaded, Router } from "vue-router"; import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import { Capacitor } from "@capacitor/core";
import QuickNav from "../components/QuickNav.vue"; import QuickNav from "../components/QuickNav.vue";
import EntityIcon from "../components/EntityIcon.vue"; import EntityIcon from "../components/EntityIcon.vue";
@ -1438,5 +1439,13 @@ export default class ContactsView extends Vue {
); );
} }
} }
private handleQRCodeClick() {
if (Capacitor.isNativePlatform()) {
this.$router.push({ name: "contact-qr-scan-full" });
} else {
this.$router.push({ name: "contact-qr" });
}
}
} }
</script> </script>

Loading…
Cancel
Save