forked from jsnbuchanan/crowd-funder-for-time-pwa
Remove ContactScanView and update QR scan view naming for consistency
- Deleted ContactScanView.vue and removed its route from the router - Renamed ContactQRScanView.vue to ContactQRScanFullView.vue - Updated all router paths, names, and references to use 'contact-qr-scan-full' - Updated related router links in ContactQRScanShowView.vue for consistency - Ensured all naming and routing is consolidated and matches the new view/component name
This commit is contained in:
@@ -88,9 +88,9 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
component: () => import("../views/ContactQRScanShowView.vue"),
|
component: () => import("../views/ContactQRScanShowView.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/contact-qr-scan",
|
path: "/contact-qr-scan-full",
|
||||||
name: "contact-qr-scan",
|
name: "contact-qr-scan-full",
|
||||||
component: () => import("../views/ContactQRScanView.vue"),
|
component: () => import("../views/ContactQRScanFullView.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/contacts",
|
path: "/contacts",
|
||||||
|
|||||||
@@ -172,7 +172,7 @@
|
|||||||
<button
|
<button
|
||||||
v-if="isNativePlatform"
|
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"
|
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' })"
|
@click="$router.push({ name: 'contact-qr-scan-full' })"
|
||||||
>
|
>
|
||||||
Scan QR Code
|
Scan QR Code
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,430 +0,0 @@
|
|||||||
<template>
|
|
||||||
<!-- CONTENT -->
|
|
||||||
<section id="Content" class="relativew-[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)]"
|
|
||||||
>
|
|
||||||
<p class="text-center text-white mb-3">
|
|
||||||
Point your camera at a TimeSafari contact QR code to scan it
|
|
||||||
automatically.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p v-if="error" class="text-center text-rose-300 mb-3">{{ error }}</p>
|
|
||||||
|
|
||||||
<div class="flex justify-center items-center">
|
|
||||||
<button
|
|
||||||
class="text-center text-slate-600 leading-none bg-white p-2 rounded-full drop-shadow-lg"
|
|
||||||
@click="handleBack"
|
|
||||||
>
|
|
||||||
<font-awesome icon="xmark" class="size-6"></font-awesome>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
|
||||||
import { Router } from "vue-router";
|
|
||||||
import { logger } from "../utils/logger";
|
|
||||||
import { QRScannerFactory } from "../services/QRScanner/QRScannerFactory";
|
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
|
||||||
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 { retrieveSettingsForActiveAccount } from "../db/index";
|
|
||||||
import { setVisibilityUtil } from "../libs/endorserServer";
|
|
||||||
|
|
||||||
interface QRScanResult {
|
|
||||||
rawValue?: string;
|
|
||||||
barcode?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: {
|
|
||||||
QuickNav,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class ContactQRScan extends Vue {
|
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
||||||
$router!: Router;
|
|
||||||
|
|
||||||
isScanning = false;
|
|
||||||
error: string | null = null;
|
|
||||||
activeDid = "";
|
|
||||||
apiServer = "";
|
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
// Add cleanup tracking
|
|
||||||
private isCleaningUp = false;
|
|
||||||
private isMounted = false;
|
|
||||||
|
|
||||||
async created() {
|
|
||||||
try {
|
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
|
||||||
this.activeDid = settings.activeDid || "";
|
|
||||||
this.apiServer = settings.apiServer || "";
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error initializing component:", {
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
stack: error instanceof Error ? error.stack : undefined,
|
|
||||||
});
|
|
||||||
this.$notify({
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Initialization Error",
|
|
||||||
text: "Failed to initialize QR scanner. Please try again.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "warning",
|
|
||||||
title: "HTTPS Required",
|
|
||||||
text: "Camera access requires a secure (HTTPS) connection",
|
|
||||||
},
|
|
||||||
5000,
|
|
||||||
);
|
|
||||||
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(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "warning",
|
|
||||||
title: "Camera Access Required",
|
|
||||||
text: "Camera permission denied",
|
|
||||||
},
|
|
||||||
5000,
|
|
||||||
);
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Extract JWT
|
|
||||||
const jwt = getContactJwtFromJwtUrl(rawValue);
|
|
||||||
if (!jwt) {
|
|
||||||
logger.warn("Invalid QR code format - no JWT found in URL");
|
|
||||||
this.$notify({
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Invalid QR Code",
|
|
||||||
text: "This QR code does not contain valid contact information. Please scan a TimeSafari contact QR code.",
|
|
||||||
});
|
|
||||||
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({
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Invalid Contact Info",
|
|
||||||
text: "The contact information is incomplete or invalid.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const contactInfo = decodedJwt.payload.own;
|
|
||||||
if (!contactInfo.did) {
|
|
||||||
logger.warn("Invalid contact info - missing DID");
|
|
||||||
this.$notify({
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Invalid Contact",
|
|
||||||
text: "The contact DID is missing.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create contact object
|
|
||||||
const contact = {
|
|
||||||
did: contactInfo.did,
|
|
||||||
name: contactInfo.name || "",
|
|
||||||
email: contactInfo.email || "",
|
|
||||||
phone: contactInfo.phone || "",
|
|
||||||
company: contactInfo.company || "",
|
|
||||||
title: contactInfo.title || "",
|
|
||||||
notes: contactInfo.notes || "",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add contact and stop 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),
|
|
||||||
stack: error instanceof Error ? error.stack : undefined,
|
|
||||||
});
|
|
||||||
this.$notify({
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text:
|
|
||||||
error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: "Could not process QR code. Please try again.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onScanError(error: Error) {
|
|
||||||
this.error = error.message;
|
|
||||||
logger.error("QR code scan error:", {
|
|
||||||
error: error.message,
|
|
||||||
stack: error.stack,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async setVisibility(contact: Contact, visibility: boolean) {
|
|
||||||
const result = await setVisibilityUtil(
|
|
||||||
this.activeDid,
|
|
||||||
this.apiServer,
|
|
||||||
this.axios,
|
|
||||||
db,
|
|
||||||
contact,
|
|
||||||
visibility,
|
|
||||||
);
|
|
||||||
if (result.error) {
|
|
||||||
this.$notify({
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error Setting Visibility",
|
|
||||||
text: result.error as string,
|
|
||||||
});
|
|
||||||
} else if (!result.success) {
|
|
||||||
logger.warn("Unexpected result from setting visibility:", result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async addNewContact(contact: Contact) {
|
|
||||||
try {
|
|
||||||
logger.info("Opening database connection for new contact");
|
|
||||||
await db.open();
|
|
||||||
|
|
||||||
// Check if contact already exists
|
|
||||||
const existingContacts = await db.contacts.toArray();
|
|
||||||
const existingContact = existingContacts.find(
|
|
||||||
(c) => c.did === contact.did,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingContact) {
|
|
||||||
logger.info("Contact already exists", { did: contact.did });
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "warning",
|
|
||||||
title: "Contact Exists",
|
|
||||||
text: "This contact has already been added to your list.",
|
|
||||||
},
|
|
||||||
3000,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new contact
|
|
||||||
await db.contacts.add(contact);
|
|
||||||
|
|
||||||
if (this.activeDid) {
|
|
||||||
logger.info("Setting contact visibility", { did: contact.did });
|
|
||||||
await this.setVisibility(contact, true);
|
|
||||||
contact.seesMe = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "success",
|
|
||||||
title: "Contact Added",
|
|
||||||
text: this.activeDid
|
|
||||||
? "They were added, and your activity is visible to them."
|
|
||||||
: "They were added.",
|
|
||||||
},
|
|
||||||
3000,
|
|
||||||
);
|
|
||||||
} 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(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Contact Error",
|
|
||||||
text: "Could not save contact. Check if it already exists.",
|
|
||||||
},
|
|
||||||
5000,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lifecycle hooks
|
|
||||||
mounted() {
|
|
||||||
this.isMounted = true;
|
|
||||||
document.addEventListener("pause", this.handleAppPause);
|
|
||||||
document.addEventListener("resume", this.handleAppResume);
|
|
||||||
this.startScanning(); // Automatically start scanning when view is mounted
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeDestroy() {
|
|
||||||
this.isMounted = false;
|
|
||||||
document.removeEventListener("pause", this.handleAppPause);
|
|
||||||
document.removeEventListener("resume", this.handleAppResume);
|
|
||||||
this.cleanupScanner();
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleAppPause() {
|
|
||||||
if (!this.isMounted) return;
|
|
||||||
|
|
||||||
logger.info("App paused, stopping scanner");
|
|
||||||
await this.stopScanning();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleAppResume() {
|
|
||||||
if (!this.isMounted) return;
|
|
||||||
|
|
||||||
logger.info("App resumed, scanner can be restarted by user");
|
|
||||||
this.isScanning = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleBack() {
|
|
||||||
await this.cleanupScanner();
|
|
||||||
this.$router.back();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.aspect-square {
|
|
||||||
aspect-ratio: 1 / 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
<template>
|
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
|
||||||
<!-- Breadcrumb -->
|
|
||||||
<div id="ViewBreadcrumb" class="mb-8">
|
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
|
||||||
<!-- Cancel -->
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'account' }"
|
|
||||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
|
||||||
><font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
|
||||||
</router-link>
|
|
||||||
|
|
||||||
Scan Contact
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="text-sm uppercase font-semibold mb-2">Scan a QR Code…</h3>
|
|
||||||
<div class="bg-black rounded overflow-hidden relative mb-4">
|
|
||||||
<img src="https://picsum.photos/400/400?random=1" class="w-full" />
|
|
||||||
|
|
||||||
<!-- Darken overlay -->
|
|
||||||
<!-- Top -->
|
|
||||||
<div class="absolute top-0 left-0 right-0 bg-black/50 h-1/4"></div>
|
|
||||||
<!-- Reft -->
|
|
||||||
<div class="absolute top-1/4 bottom-1/4 left-0 bg-black/50 w-1/4"></div>
|
|
||||||
<!-- Right -->
|
|
||||||
<div class="absolute top-1/4 bottom-1/4 right-0 bg-black/50 w-1/4"></div>
|
|
||||||
<!-- Bottom -->
|
|
||||||
<div class="absolute bottom-0 left-0 right-0 bg-black/50 h-1/4"></div>
|
|
||||||
|
|
||||||
<!-- Reticle overlay -->
|
|
||||||
<!-- Top-left -->
|
|
||||||
<div
|
|
||||||
class="absolute top-1/4 left-1/4 h-6 w-6 border-white border-t-4 border-l-4 drop-shadow"
|
|
||||||
></div>
|
|
||||||
<!-- Top-right -->
|
|
||||||
<div
|
|
||||||
class="absolute top-1/4 right-1/4 h-6 w-6 border-white border-t-4 border-r-4 drop-shadow"
|
|
||||||
></div>
|
|
||||||
<!-- Bottom-left -->
|
|
||||||
<div
|
|
||||||
class="absolute bottom-1/4 left-1/4 h-6 w-6 border-white border-b-4 border-l-4 drop-shadow"
|
|
||||||
></div>
|
|
||||||
<!-- Bottom-right -->
|
|
||||||
<div
|
|
||||||
class="absolute bottom-1/4 right-1/4 h-6 w-6 border-white border-b-4 border-r-4 drop-shadow"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="text-sm uppercase font-semibold mb-2">…or Enter Contact Data</h3>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Name (optional)"
|
|
||||||
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="ID"
|
|
||||||
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Public Key (optional)"
|
|
||||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="mt-8">
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
||||||
<input
|
|
||||||
type="submit"
|
|
||||||
class="block w-full text-center text-lg font-bold 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-2 py-3 rounded-md"
|
|
||||||
value="Look Up Contact"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: {},
|
|
||||||
})
|
|
||||||
export default class ContactScanView extends Vue {}
|
|
||||||
</script>
|
|
||||||
Reference in New Issue
Block a user