Error: {{ error }}
{{ cameraStateMessage || "Ready to scan" }}
@@ -168,10 +159,7 @@ import { QrcodeStream } from "vue-qrcode-reader";
import QuickNav from "../components/QuickNav.vue";
import UserNameDialog from "../components/UserNameDialog.vue";
import { NotificationIface } from "../constants/app";
-import { db } from "../db/index";
import { Contact } from "../db/tables/contacts";
-import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
-import * as databaseUtil from "../db/databaseUtil";
import { parseJsonField } from "../db/databaseUtil";
import { getContactJwtFromJwtUrl } from "../libs/crypto";
import {
@@ -187,8 +175,34 @@ import { Router } from "vue-router";
import { logger } from "../utils/logger";
import { QRScannerFactory } from "@/services/QRScanner/QRScannerFactory";
import { CameraState } from "@/services/QRScanner/types";
-import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
+import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { Account } from "@/db/tables/accounts";
+import { createNotifyHelpers } from "@/utils/notify";
+import {
+ NOTIFY_QR_INITIALIZATION_ERROR,
+ NOTIFY_QR_CAMERA_IN_USE,
+ NOTIFY_QR_CAMERA_ACCESS_REQUIRED,
+ NOTIFY_QR_NO_CAMERA,
+ NOTIFY_QR_HTTPS_REQUIRED,
+ NOTIFY_QR_CONTACT_EXISTS,
+ NOTIFY_QR_CONTACT_ERROR,
+ NOTIFY_QR_REGISTRATION_SUBMITTED,
+ NOTIFY_QR_REGISTRATION_ERROR,
+ NOTIFY_QR_URL_COPIED,
+ NOTIFY_QR_CODE_HELP,
+ NOTIFY_QR_DID_COPIED,
+ NOTIFY_QR_INVALID_QR_CODE,
+ NOTIFY_QR_INVALID_CONTACT_INFO,
+ NOTIFY_QR_MISSING_DID,
+ NOTIFY_QR_UNKNOWN_CONTACT_TYPE,
+ NOTIFY_QR_PROCESSING_ERROR,
+ createQRContactAddedMessage,
+ createQRRegistrationSuccessMessage,
+ QR_TIMEOUT_SHORT,
+ QR_TIMEOUT_MEDIUM,
+ QR_TIMEOUT_STANDARD,
+ QR_TIMEOUT_LONG,
+} from "@/constants/notifications";
interface QRScanResult {
rawValue?: string;
@@ -206,13 +220,22 @@ interface IUserNameDialog {
UserNameDialog,
QrcodeStream,
},
+ mixins: [PlatformServiceMixin],
})
export default class ContactQRScanShow extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
+ // Notification helper system
+ private notify = createNotifyHelpers(this.$notify);
+
activeDid = "";
apiServer = "";
+
+ // Axios instance for API calls
+ get axios() {
+ return (this as any).$platformService.axios;
+ }
givenName = "";
hideRegisterPromptOnNewContact = false;
isRegistered = false;
@@ -244,9 +267,43 @@ export default class ContactQRScanShow extends Vue {
private isDesktop = false;
private isFrontCamera = false;
+ // Computed properties for template classes
+ get nameWarningClasses(): string {
+ return "bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 my-4";
+ }
+
+ get setNameButtonClasses(): string {
+ return "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";
+ }
+
+ get qrCodeContainerClasses(): string {
+ return "block w-[90vw] max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto my-4";
+ }
+
+ get scannerContainerClasses(): string {
+ return "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";
+ }
+
+ get statusMessageClasses(): string {
+ return "absolute top-0 left-0 right-0 bg-black bg-opacity-50 text-white text-sm text-center py-2 z-10";
+ }
+
+ get cameraStatusIndicatorClasses(): Record
{
+ return {
+ 'inline-block w-2 h-2 rounded-full': true,
+ 'bg-green-500': this.cameraState === 'ready',
+ 'bg-yellow-500': this.cameraState === 'in_use',
+ 'bg-red-500':
+ this.cameraState === 'error' ||
+ this.cameraState === 'permission_denied' ||
+ this.cameraState === 'not_found',
+ 'bg-blue-500': this.cameraState === 'off',
+ };
+ }
+
async created() {
try {
- const settings = await databaseUtil.retrieveSettingsForActiveAccount();
+ const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.givenName = settings.firstName || "";
@@ -274,12 +331,7 @@ export default class ContactQRScanShow extends Vue {
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 renderer or scanner. Please try again.",
- });
+ this.notify.error(NOTIFY_QR_INITIALIZATION_ERROR.message);
}
}
@@ -313,41 +365,17 @@ export default class ContactQRScanShow extends Vue {
case "in_use":
this.error = "Camera is in use by another application";
this.isScanning = false;
- this.$notify(
- {
- group: "alert",
- type: "warning",
- title: "Camera in Use",
- text: "Please close other applications using the camera and try again",
- },
- 5000,
- );
+ this.notify.warning(NOTIFY_QR_CAMERA_IN_USE.message, QR_TIMEOUT_LONG);
break;
case "permission_denied":
this.error = "Camera permission denied";
this.isScanning = false;
- this.$notify(
- {
- group: "alert",
- type: "warning",
- title: "Camera Access Required",
- text: "Please grant camera permission to scan QR codes",
- },
- 5000,
- );
+ this.notify.warning(NOTIFY_QR_CAMERA_ACCESS_REQUIRED.message, QR_TIMEOUT_LONG);
break;
case "not_found":
this.error = "No camera found";
this.isScanning = false;
- this.$notify(
- {
- group: "alert",
- type: "warning",
- title: "No Camera",
- text: "No camera was found on this device",
- },
- 5000,
- );
+ this.notify.warning(NOTIFY_QR_NO_CAMERA.message, QR_TIMEOUT_LONG);
break;
case "error":
this.error = this.cameraStateMessage || "Camera error";
@@ -362,15 +390,7 @@ export default class ContactQRScanShow extends Vue {
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,
- );
+ this.notify.warning(NOTIFY_QR_HTTPS_REQUIRED.message, QR_TIMEOUT_LONG);
return;
}
@@ -422,18 +442,6 @@ export default class ContactQRScanShow extends Vue {
}
}
- danger(message: string, title: string = "Error", timeout = 5000) {
- this.$notify(
- {
- group: "alert",
- type: "danger",
- title: title,
- text: message,
- },
- timeout,
- );
- }
-
/**
* Handle QR code scan result with debouncing to prevent duplicate scans
*/
@@ -470,12 +478,7 @@ export default class ContactQRScanShow extends Vue {
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. Scan a TimeSafari contact QR code.",
- });
+ this.notify.error(NOTIFY_QR_INVALID_QR_CODE.message);
return;
}
logger.info("Decoding JWT payload from QR code");
@@ -484,12 +487,7 @@ export default class ContactQRScanShow extends Vue {
// Process JWT and contact info
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.",
- });
+ this.notify.error(NOTIFY_QR_INVALID_CONTACT_INFO.message);
return;
}
@@ -497,12 +495,7 @@ export default class ContactQRScanShow extends Vue {
const did = contactInfo.did || decodedJwt.payload.iss;
if (!did) {
logger.warn("Invalid contact info - missing DID");
- this.$notify({
- group: "alert",
- type: "danger",
- title: "Invalid Contact",
- text: "The contact DID is missing.",
- });
+ this.notify.error(NOTIFY_QR_MISSING_DID.message);
return;
}
@@ -518,12 +511,7 @@ export default class ContactQRScanShow extends Vue {
const lines = rawValue.split(/\n/);
contact = libsUtil.csvLineToContact(lines[1]);
} else {
- this.$notify({
- group: "alert",
- type: "danger",
- title: "Error",
- text: "Could not determine the type of contact info. Try again, or tap the QR code to copy it and send it to them.",
- });
+ this.notify.error(NOTIFY_QR_UNKNOWN_CONTACT_TYPE.message);
return;
}
@@ -538,15 +526,11 @@ export default class ContactQRScanShow extends Vue {
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.",
- });
+ this.notify.error(
+ error instanceof Error
+ ? error.message
+ : NOTIFY_QR_PROCESSING_ERROR.message
+ );
}
}
@@ -555,12 +539,11 @@ export default class ContactQRScanShow extends Vue {
this.activeDid,
this.apiServer,
this.axios,
- db,
contact,
visibility,
);
if (result.error) {
- this.danger(result.error as string, "Error Setting Visibility");
+ this.notify.error(result.error as string, QR_TIMEOUT_LONG);
} else if (!result.success) {
logger.warn("Unexpected result from setting visibility:", result);
}
@@ -571,15 +554,7 @@ export default class ContactQRScanShow extends Vue {
did: contact.did,
name: contact.name,
});
- this.$notify(
- {
- group: "alert",
- type: "toast",
- text: "",
- title: "Registration submitted...",
- },
- 1000,
- );
+ this.notify.toast(NOTIFY_QR_REGISTRATION_SUBMITTED.message, QR_TIMEOUT_SHORT);
try {
const regResult = await register(
@@ -590,34 +565,17 @@ export default class ContactQRScanShow extends Vue {
);
if (regResult.success) {
contact.registered = true;
- const platformService = PlatformServiceFactory.getInstance();
- await platformService.dbExec(
- "UPDATE contacts SET registered = ? WHERE did = ?",
- [true, contact.did],
- );
+ await this.$updateContact(contact.did, { registered: true });
logger.info("Contact registration successful", { did: contact.did });
- this.$notify(
- {
- group: "alert",
- type: "success",
- title: "Registration Success",
- text:
- (contact.name || "That unnamed person") + " has been registered.",
- },
- 5000,
+ this.notify.success(
+ createQRRegistrationSuccessMessage(contact.name || ""),
+ QR_TIMEOUT_LONG,
);
} else {
- this.$notify(
- {
- group: "alert",
- type: "danger",
- title: "Registration Error",
- text:
- (regResult.error as string) ||
- "Something went wrong during registration.",
- },
- 5000,
+ this.notify.error(
+ (regResult.error as string) || NOTIFY_QR_REGISTRATION_ERROR.message,
+ QR_TIMEOUT_LONG,
);
}
} catch (error) {
@@ -645,15 +603,7 @@ export default class ContactQRScanShow extends Vue {
userMessage = error as string;
}
// Now set that error for the user to see.
- this.$notify(
- {
- group: "alert",
- type: "danger",
- title: "Registration Error",
- text: userMessage,
- },
- 5000,
- );
+ this.notify.error(userMessage, QR_TIMEOUT_LONG);
}
}
@@ -679,28 +629,12 @@ export default class ContactQRScanShow extends Vue {
useClipboard()
.copy(jwtUrl)
.then(() => {
- this.$notify(
- {
- group: "alert",
- type: "toast",
- title: "Copied",
- text: "Contact URL was copied to clipboard.",
- },
- 2000,
- );
+ this.notify.toast(NOTIFY_QR_URL_COPIED.message, QR_TIMEOUT_MEDIUM);
});
}
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,
- );
+ this.notify.info(NOTIFY_QR_CODE_HELP.message, QR_TIMEOUT_LONG);
}
onCopyDidToClipboard() {
@@ -708,15 +642,7 @@ export default class ContactQRScanShow extends Vue {
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,
- );
+ this.notify.info(NOTIFY_QR_DID_COPIED.message, QR_TIMEOUT_LONG);
});
}
@@ -772,27 +698,11 @@ export default class ContactQRScanShow extends Vue {
logger.info("Opening database connection for new contact");
// Check if contact already exists
- const platformService = PlatformServiceFactory.getInstance();
- const dbAllContacts = await platformService.dbQuery(
- "SELECT * FROM contacts WHERE did = ?",
- [contact.did],
- );
- const existingContacts = databaseUtil.mapQueryResultToValues(
- dbAllContacts,
- ) as unknown as Contact[];
- const existingContact: Contact | undefined = existingContacts[0];
+ const existingContact = await this.$getContact(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.",
- },
- 5000,
- );
+ this.notify.warning(NOTIFY_QR_CONTACT_EXISTS.message, QR_TIMEOUT_LONG);
return;
}
@@ -801,11 +711,7 @@ export default class ContactQRScanShow extends Vue {
contact.contactMethods = JSON.stringify(
parseJsonField(contact.contactMethods, []),
);
- const { sql, params } = databaseUtil.generateInsertStatement(
- contact as unknown as Record,
- "contacts",
- );
- await platformService.dbExec(sql, params);
+ await this.$insertContact(contact);
if (this.activeDid) {
logger.info("Setting contact visibility", { did: contact.did });
@@ -813,17 +719,7 @@ export default class ContactQRScanShow extends Vue {
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,
- );
+ this.notify.success(createQRContactAddedMessage(!!this.activeDid), QR_TIMEOUT_STANDARD);
if (
this.isRegistered &&
@@ -831,29 +727,23 @@ export default class ContactQRScanShow extends Vue {
!contact.registered
) {
setTimeout(() => {
- this.$notify(
+ this.notify.confirm(
+ "Register",
+ "Do you want to register them?",
{
- group: "modal",
- type: "confirm",
- title: "Register",
- text: "Do you want to register them?",
onCancel: async (stopAsking?: boolean) => {
if (stopAsking) {
- const platformService = PlatformServiceFactory.getInstance();
- await platformService.dbExec(
- "UPDATE settings SET hideRegisterPromptOnNewContact = ? WHERE id = ?",
- [stopAsking, MASTER_SETTINGS_KEY],
- );
+ await this.$updateSettings({
+ hideRegisterPromptOnNewContact: stopAsking,
+ });
this.hideRegisterPromptOnNewContact = stopAsking;
}
},
onNo: async (stopAsking?: boolean) => {
if (stopAsking) {
- const platformService = PlatformServiceFactory.getInstance();
- await platformService.dbExec(
- "UPDATE settings SET hideRegisterPromptOnNewContact = ? WHERE id = ?",
- [stopAsking, MASTER_SETTINGS_KEY],
- );
+ await this.$updateSettings({
+ hideRegisterPromptOnNewContact: stopAsking,
+ });
this.hideRegisterPromptOnNewContact = stopAsking;
}
},
@@ -872,15 +762,7 @@ export default class ContactQRScanShow extends Vue {
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,
- );
+ this.notify.error(NOTIFY_QR_CONTACT_ERROR.message, QR_TIMEOUT_LONG);
}
}