You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
691 lines
20 KiB
691 lines
20 KiB
<template>
|
|
<!-- CONTENT -->
|
|
<section id="Content" class="relative w-[100vw] h-[100vh]">
|
|
<div :class="mainContentClasses">
|
|
<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>
|
|
|
|
<!-- 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>
|
|
|
|
Share Contact Info
|
|
</h1>
|
|
</div>
|
|
|
|
<div
|
|
v-if="shouldShowNameWarning"
|
|
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="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"
|
|
>
|
|
Set Your Name
|
|
</button>
|
|
</div>
|
|
|
|
<UserNameDialog ref="userNameDialog" />
|
|
|
|
<div
|
|
v-if="hasEthrDid"
|
|
:class="qrContainerClasses"
|
|
@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="hasAnyDid" 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="cameraFrameClasses">
|
|
<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>
|
|
|
|
<script lang="ts">
|
|
import { Buffer } from "buffer/";
|
|
import QRCodeVue3 from "qr-code-generator-vue3";
|
|
import { Component, Vue } from "vue-facing-decorator";
|
|
import { Router } from "vue-router";
|
|
import { useClipboard } from "@vueuse/core";
|
|
|
|
import { logger } from "../utils/logger";
|
|
import { QRScannerFactory } from "../services/QRScanner/QRScannerFactory";
|
|
import QuickNav from "../components/QuickNav.vue";
|
|
import { Contact } from "../db/tables/contacts";
|
|
import { getContactJwtFromJwtUrl } from "../libs/crypto";
|
|
import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
|
|
import * as libsUtil from "../libs/util";
|
|
import {
|
|
CONTACT_CSV_HEADER,
|
|
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
|
generateEndorserJwtUrlForAccount,
|
|
setVisibilityUtil,
|
|
} from "../libs/endorserServer";
|
|
import UserNameDialog from "../components/UserNameDialog.vue";
|
|
import { retrieveAccountMetadata } from "../libs/util";
|
|
|
|
import { Account } from "@/db/tables/accounts";
|
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
|
import {
|
|
NOTIFY_QR_INITIALIZATION_ERROR,
|
|
NOTIFY_QR_HTTPS_REQUIRED,
|
|
NOTIFY_QR_CAMERA_ACCESS_REQUIRED,
|
|
NOTIFY_QR_CONTACT_EXISTS,
|
|
NOTIFY_QR_CONTACT_ERROR,
|
|
NOTIFY_QR_INVALID_QR_CODE,
|
|
NOTIFY_QR_INVALID_CONTACT_INFO,
|
|
NOTIFY_QR_MISSING_DID,
|
|
NOTIFY_QR_UNKNOWN_CONTACT_TYPE,
|
|
NOTIFY_QR_PROCESSING_ERROR,
|
|
NOTIFY_QR_URL_COPIED,
|
|
NOTIFY_QR_CODE_HELP,
|
|
NOTIFY_QR_DID_COPIED,
|
|
createQRContactAddedMessage,
|
|
QR_TIMEOUT_MEDIUM,
|
|
QR_TIMEOUT_STANDARD,
|
|
QR_TIMEOUT_LONG,
|
|
} from "@/constants/notifications";
|
|
import { createNotifyHelpers } from "../utils/notify";
|
|
|
|
interface QRScanResult {
|
|
rawValue?: string;
|
|
barcode?: string;
|
|
}
|
|
|
|
interface IUserNameDialog {
|
|
open: (callback: (name: string) => void) => void;
|
|
}
|
|
|
|
@Component({
|
|
components: {
|
|
QuickNav,
|
|
QRCodeVue3,
|
|
UserNameDialog,
|
|
},
|
|
mixins: [PlatformServiceMixin],
|
|
})
|
|
/**
|
|
* ContactQRScanFullView.vue
|
|
*
|
|
* Enhanced QR code scanner component for exchanging contact information in TimeSafari.
|
|
* Supports both sharing user's contact info via QR code and scanning other users' QR codes.
|
|
*
|
|
* Key Features:
|
|
* - Generates QR codes for user's contact information
|
|
* - Scans QR codes from other TimeSafari users
|
|
* - Handles both JWT-based and CSV-based contact formats
|
|
* - Debounces duplicate scans to prevent processing same code multiple times
|
|
* - Manages camera permissions and lifecycle
|
|
* - Provides real-time feedback during scanning process
|
|
*
|
|
* Database Operations:
|
|
* - Retrieves user settings and profile information
|
|
* - Stores new contacts with proper validation
|
|
* - Manages contact visibility settings
|
|
*
|
|
* Security Features:
|
|
* - Validates contact information before storage
|
|
* - Encrypts sensitive data using platform services
|
|
* - Handles camera permissions securely
|
|
* - Prevents duplicate contact entries
|
|
*
|
|
* @author Matthew Raymer
|
|
* @since 2024
|
|
*/
|
|
export default class ContactQRScanFull extends Vue {
|
|
$notify!: (notification: any, timeout?: number) => void;
|
|
$router!: Router;
|
|
|
|
// Notification helper system
|
|
private notify = createNotifyHelpers(this.$notify);
|
|
|
|
isScanning = false;
|
|
error: string | null = null;
|
|
activeDid = "";
|
|
apiServer = "";
|
|
givenName = "";
|
|
isRegistered = false;
|
|
profileImageUrl = "";
|
|
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 = 5000; // Increased from 2000 to 5000ms to better handle mobile scanning
|
|
|
|
// Add cleanup tracking
|
|
private isCleaningUp = false;
|
|
private isMounted = false;
|
|
|
|
/**
|
|
* Computed property for QR code container CSS classes
|
|
*/
|
|
get qrContainerClasses(): string {
|
|
return "block w-full max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto mt-4";
|
|
}
|
|
|
|
/**
|
|
* Computed property for camera frame CSS classes
|
|
*/
|
|
get cameraFrameClasses(): string {
|
|
return "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";
|
|
}
|
|
|
|
/**
|
|
* Computed property for main content container CSS classes
|
|
*/
|
|
get mainContentClasses(): string {
|
|
return "p-6 bg-white w-full max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto";
|
|
}
|
|
|
|
/**
|
|
* Computed property to determine if user has an ETHR DID
|
|
*/
|
|
get hasEthrDid(): boolean {
|
|
return !!(this.activeDid && this.activeDid.startsWith(ETHR_DID_PREFIX));
|
|
}
|
|
|
|
/**
|
|
* Computed property to determine if user has any DID
|
|
*/
|
|
get hasAnyDid(): boolean {
|
|
return !!this.activeDid;
|
|
}
|
|
|
|
/**
|
|
* Computed property to determine if user should be shown the name setup warning
|
|
*/
|
|
get shouldShowNameWarning(): boolean {
|
|
return !this.givenName;
|
|
}
|
|
|
|
/**
|
|
* Vue lifecycle hook - component initialization
|
|
* Loads user settings and generates QR code for contact sharing
|
|
*/
|
|
async created() {
|
|
try {
|
|
const settings = await this.$accountSettings();
|
|
this.activeDid = settings.activeDid || "";
|
|
this.apiServer = settings.apiServer || "";
|
|
this.givenName = settings.firstName || "";
|
|
this.isRegistered = !!settings.isRegistered;
|
|
this.profileImageUrl = settings.profileImageUrl || "";
|
|
|
|
const account = await retrieveAccountMetadata(this.activeDid);
|
|
if (account) {
|
|
const name =
|
|
(settings.firstName || "") +
|
|
(settings.lastName ? ` ${settings.lastName}` : "");
|
|
const publicKeyBase64 = Buffer.from(
|
|
account.publicKeyHex,
|
|
"hex",
|
|
).toString("base64");
|
|
this.qrValue =
|
|
CONTACT_CSV_HEADER +
|
|
"\n" +
|
|
`"${name}",${account.did},${publicKeyBase64},false,${this.isRegistered}`;
|
|
}
|
|
} catch (error) {
|
|
logger.error("Error initializing component:", {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
});
|
|
this.notify.error(
|
|
NOTIFY_QR_INITIALIZATION_ERROR.message,
|
|
QR_TIMEOUT_LONG,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Starts the QR code scanning process
|
|
* Handles permission requests and initializes camera access
|
|
* @throws Will log error and show notification if scanning fails to start
|
|
*/
|
|
async startScanning() {
|
|
if (this.isCleaningUp) {
|
|
logger.debug("Cannot start scanning during cleanup");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.error = null;
|
|
this.isScanning = true;
|
|
this.lastScannedValue = "";
|
|
this.lastScanTime = 0;
|
|
|
|
const scanner = QRScannerFactory.getInstance();
|
|
|
|
// Check if scanning is supported first
|
|
if (!(await scanner.isSupported())) {
|
|
this.error =
|
|
"Camera access requires HTTPS. Please use a secure connection.";
|
|
this.isScanning = false;
|
|
this.notify.warning(NOTIFY_QR_HTTPS_REQUIRED.message, QR_TIMEOUT_LONG);
|
|
return;
|
|
}
|
|
|
|
// Check permissions first
|
|
if (!(await scanner.checkPermissions())) {
|
|
const granted = await scanner.requestPermissions();
|
|
if (!granted) {
|
|
this.error = "Camera permission denied";
|
|
this.isScanning = false;
|
|
// Show notification for better visibility
|
|
this.notify.warning(
|
|
NOTIFY_QR_CAMERA_ACCESS_REQUIRED.message,
|
|
QR_TIMEOUT_LONG,
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Add scan listener
|
|
scanner.addListener({
|
|
onScan: this.onScanDetect,
|
|
onError: this.onScanError,
|
|
});
|
|
|
|
// Start scanning
|
|
await scanner.startScan();
|
|
} catch (error) {
|
|
this.error = error instanceof Error ? error.message : String(error);
|
|
this.isScanning = false;
|
|
logger.error("Error starting scan:", {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stops the QR code scanning process and cleans up scan state
|
|
* @throws Will log error if stopping scan fails
|
|
*/
|
|
async stopScanning() {
|
|
try {
|
|
const scanner = QRScannerFactory.getInstance();
|
|
await scanner.stopScan();
|
|
} catch (error) {
|
|
logger.error("Error stopping scan:", {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
});
|
|
} finally {
|
|
this.isScanning = false;
|
|
this.lastScannedValue = "";
|
|
this.lastScanTime = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cleans up QR scanner resources and prevents memory leaks
|
|
* @throws Will log error if cleanup fails
|
|
*/
|
|
async cleanupScanner() {
|
|
if (this.isCleaningUp) {
|
|
return;
|
|
}
|
|
|
|
this.isCleaningUp = true;
|
|
try {
|
|
logger.info("Cleaning up QR scanner resources");
|
|
await this.stopScanning();
|
|
await QRScannerFactory.cleanup();
|
|
} catch (error) {
|
|
logger.error("Error during scanner cleanup:", {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
});
|
|
} finally {
|
|
this.isCleaningUp = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle QR code scan result with debouncing to prevent duplicate scans
|
|
*/
|
|
async onScanDetect(result: string | QRScanResult) {
|
|
try {
|
|
// Extract raw value from different possible formats
|
|
const rawValue =
|
|
typeof result === "string"
|
|
? result
|
|
: result?.rawValue || result?.barcode;
|
|
if (!rawValue) {
|
|
logger.warn("Invalid scan result - no value found:", result);
|
|
return;
|
|
}
|
|
|
|
// Debounce duplicate scans
|
|
const now = Date.now();
|
|
if (
|
|
rawValue === this.lastScannedValue &&
|
|
now - this.lastScanTime < this.SCAN_DEBOUNCE_MS
|
|
) {
|
|
logger.info("Ignoring duplicate scan:", rawValue);
|
|
return;
|
|
}
|
|
|
|
// Update scan tracking
|
|
this.lastScannedValue = rawValue;
|
|
this.lastScanTime = now;
|
|
|
|
logger.info("Processing QR code scan result:", rawValue);
|
|
|
|
let contact: Contact;
|
|
if (rawValue.includes(CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI)) {
|
|
// Extract JWT
|
|
const jwt = getContactJwtFromJwtUrl(rawValue);
|
|
if (!jwt) {
|
|
logger.warn("Invalid QR code format - no JWT found in URL");
|
|
this.notify.error(NOTIFY_QR_INVALID_QR_CODE.message, QR_TIMEOUT_LONG);
|
|
return;
|
|
}
|
|
|
|
// Process JWT and contact info
|
|
logger.info("Decoding JWT payload from QR code");
|
|
const decodedJwt = await decodeEndorserJwt(jwt);
|
|
if (!decodedJwt?.payload?.own) {
|
|
logger.warn("Invalid JWT payload - missing 'own' field");
|
|
this.notify.error(
|
|
NOTIFY_QR_INVALID_CONTACT_INFO.message,
|
|
QR_TIMEOUT_LONG,
|
|
);
|
|
return;
|
|
}
|
|
|
|
const contactInfo = decodedJwt.payload.own;
|
|
const did = contactInfo.did || decodedJwt.payload.iss;
|
|
if (!did) {
|
|
logger.warn("Invalid contact info - missing DID");
|
|
this.notify.error(NOTIFY_QR_MISSING_DID.message, QR_TIMEOUT_LONG);
|
|
return;
|
|
}
|
|
|
|
// Create contact object
|
|
contact = {
|
|
did: did,
|
|
name: contactInfo.name || "",
|
|
publicKeyBase64: contactInfo.publicKeyBase64 || "",
|
|
seesMe: contactInfo.seesMe || false,
|
|
registered: contactInfo.registered || false,
|
|
};
|
|
} else if (rawValue.startsWith(CONTACT_CSV_HEADER)) {
|
|
const lines = rawValue.split(/\n/);
|
|
contact = libsUtil.csvLineToContact(lines[1]);
|
|
} else {
|
|
this.notify.error(
|
|
NOTIFY_QR_UNKNOWN_CONTACT_TYPE.message,
|
|
QR_TIMEOUT_LONG,
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Add contact but keep scanning
|
|
logger.info("Adding new contact to database:", {
|
|
did: contact.did,
|
|
name: contact.name,
|
|
});
|
|
await this.addNewContact(contact);
|
|
} catch (error) {
|
|
logger.error("Error processing contact QR code:", {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
});
|
|
this.notify.error(
|
|
error instanceof Error
|
|
? error.message
|
|
: NOTIFY_QR_PROCESSING_ERROR.message,
|
|
QR_TIMEOUT_LONG,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles QR code scan errors
|
|
* @param error - Error object from scanner
|
|
*/
|
|
onScanError(error: Error) {
|
|
this.error = error.message;
|
|
logger.error("QR code scan error:", {
|
|
error: error.message,
|
|
stack: error.stack,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sets the visibility of a contact (whether they can see user's activity)
|
|
* @param contact - Contact object to set visibility for
|
|
* @param visibility - Whether contact should be able to see user's activity
|
|
* @throws Will log error and show notification if visibility setting fails
|
|
*/
|
|
async setVisibility(contact: Contact, visibility: boolean) {
|
|
const result = await setVisibilityUtil(
|
|
this.activeDid,
|
|
this.apiServer,
|
|
this.axios,
|
|
contact,
|
|
visibility,
|
|
);
|
|
if (result.error) {
|
|
this.notify.error(result.error as string, QR_TIMEOUT_LONG);
|
|
} else if (!result.success) {
|
|
logger.warn("Unexpected result from setting visibility:", result);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds a new contact to the database after validation
|
|
* @param contact - Contact object to add
|
|
* @throws Will log error and show notification if contact addition fails
|
|
*/
|
|
async addNewContact(contact: Contact) {
|
|
try {
|
|
logger.info("Opening database connection for new contact");
|
|
|
|
// Check if contact already exists
|
|
const existingContact = await this.$getContact(contact.did);
|
|
|
|
if (existingContact) {
|
|
logger.info("Contact already exists", { did: contact.did });
|
|
this.notify.warning(NOTIFY_QR_CONTACT_EXISTS.message, QR_TIMEOUT_LONG);
|
|
return;
|
|
}
|
|
|
|
// Add new contact
|
|
// @ts-expect-error because we're just using the value to store to the DB
|
|
contact.contactMethods = JSON.stringify(
|
|
(this as any)._parseJsonField(contact.contactMethods, []),
|
|
);
|
|
await this.$insertContact(contact);
|
|
|
|
if (this.activeDid) {
|
|
logger.info("Setting contact visibility", { did: contact.did });
|
|
await this.setVisibility(contact, true);
|
|
contact.seesMe = true;
|
|
}
|
|
|
|
this.notify.success(
|
|
createQRContactAddedMessage(!!this.activeDid),
|
|
QR_TIMEOUT_STANDARD,
|
|
);
|
|
} catch (error) {
|
|
logger.error("Error saving contact to database:", {
|
|
did: contact.did,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
});
|
|
this.notify.error(NOTIFY_QR_CONTACT_ERROR.message, QR_TIMEOUT_LONG);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Vue lifecycle hook - component mounted
|
|
* Sets up event listeners and starts scanning automatically
|
|
*/
|
|
mounted() {
|
|
this.isMounted = true;
|
|
document.addEventListener("pause", this.handleAppPause);
|
|
document.addEventListener("resume", this.handleAppResume);
|
|
this.startScanning(); // Automatically start scanning when view is mounted
|
|
}
|
|
|
|
/**
|
|
* Vue lifecycle hook - component before destruction
|
|
* Cleans up event listeners and scanner resources
|
|
*/
|
|
beforeDestroy() {
|
|
this.isMounted = false;
|
|
document.removeEventListener("pause", this.handleAppPause);
|
|
document.removeEventListener("resume", this.handleAppResume);
|
|
this.cleanupScanner();
|
|
}
|
|
|
|
/**
|
|
* Handles app pause event by stopping scanner
|
|
*/
|
|
async handleAppPause() {
|
|
if (!this.isMounted) return;
|
|
|
|
logger.info("App paused, stopping scanner");
|
|
await this.stopScanning();
|
|
}
|
|
|
|
/**
|
|
* Handles app resume event by resetting scanner state
|
|
*/
|
|
handleAppResume() {
|
|
if (!this.isMounted) return;
|
|
|
|
logger.info("App resumed, scanner can be restarted by user");
|
|
this.isScanning = false;
|
|
}
|
|
|
|
/**
|
|
* Handles back navigation with proper cleanup
|
|
*/
|
|
async handleBack() {
|
|
await this.cleanupScanner();
|
|
this.$router.back();
|
|
}
|
|
|
|
/**
|
|
* Shows help notification about QR code functionality
|
|
*/
|
|
toastQRCodeHelp() {
|
|
this.notify.info(NOTIFY_QR_CODE_HELP.message, QR_TIMEOUT_LONG);
|
|
}
|
|
|
|
/**
|
|
* Copies contact URL to clipboard for sharing
|
|
*/
|
|
async onCopyUrlToClipboard() {
|
|
const account = (await libsUtil.retrieveFullyDecryptedAccount(
|
|
this.activeDid,
|
|
)) as Account;
|
|
const jwtUrl = await generateEndorserJwtUrlForAccount(
|
|
account,
|
|
this.isRegistered,
|
|
this.givenName,
|
|
this.profileImageUrl,
|
|
true,
|
|
);
|
|
useClipboard()
|
|
.copy(jwtUrl)
|
|
.then(() => {
|
|
this.notify.toast(
|
|
NOTIFY_QR_URL_COPIED.title,
|
|
NOTIFY_QR_URL_COPIED.message,
|
|
QR_TIMEOUT_MEDIUM,
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Copies DID to clipboard for manual sharing
|
|
*/
|
|
onCopyDidToClipboard() {
|
|
useClipboard()
|
|
.copy(this.activeDid)
|
|
.then(() => {
|
|
this.notify.info(NOTIFY_QR_DID_COPIED.message, QR_TIMEOUT_LONG);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Opens the user name dialog for setting user's display name
|
|
*/
|
|
openUserNameDialog() {
|
|
(this.$refs.userNameDialog as IUserNameDialog).open((name: string) => {
|
|
this.givenName = name;
|
|
});
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.aspect-square {
|
|
aspect-ratio: 1 / 1;
|
|
}
|
|
</style>
|
|
|