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
This commit is contained in:
Jose Olarte III
2025-05-22 19:46:19 +08:00
parent 10df60316a
commit e2787e3afa
5 changed files with 229 additions and 61 deletions

View File

@@ -1,24 +1,104 @@
<template>
<!-- CONTENT -->
<section id="Content" class="relativew-[100vw] h-[100vh]">
<section id="Content" class="relative w-[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)]"
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">
Point your camera at a TimeSafari contact QR code to scan it
automatically.
</p>
<div class="mb-4">
<h1 class="text-xl text-center font-semibold relative">
<!-- Back -->
<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>
<p v-if="error" class="text-center text-rose-300 mb-3">{{ error }}</p>
<!-- 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>
<div class="flex justify-center items-center">
Share Contact Info
</h1>
</div>
<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
class="text-center text-slate-600 leading-none bg-white p-2 rounded-full drop-shadow-lg"
@click="handleBack"
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="openUserNameDialog"
>
<font-awesome icon="xmark" class="size-6"></font-awesome>
Set Your Name
</button>
</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>
</section>
</template>
@@ -33,18 +113,29 @@ 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 { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
import { retrieveSettingsForActiveAccount } from "../db/index";
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 {
rawValue?: string;
barcode?: string;
}
interface IUserNameDialog {
open: (callback: (name: string) => void) => void;
}
@Component({
components: {
QuickNav,
QRCodeVue3,
UserNameDialog,
},
})
export default class ContactQRScan extends Vue {
@@ -55,11 +146,14 @@ export default class ContactQRScan extends Vue {
error: string | null = null;
activeDid = "";
apiServer = "";
givenName = "";
qrValue = "";
ETHR_DID_PREFIX = ETHR_DID_PREFIX;
// 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
private readonly SCAN_DEBOUNCE_MS = 5000; // Increased from 2000 to 5000ms to better handle mobile scanning
// Add cleanup tracking
private isCleaningUp = false;
@@ -70,6 +164,21 @@ export default class ContactQRScan extends Vue {
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
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) {
logger.error("Error initializing component:", {
error: error instanceof Error ? error.message : String(error),
@@ -270,14 +379,12 @@ export default class ContactQRScan extends Vue {
notes: contactInfo.notes || "",
};
// Add contact and stop scanning
// Add contact but keep 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),
@@ -420,6 +527,56 @@ export default class ContactQRScan extends Vue {
await this.cleanupScanner();
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>