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:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user