Compare commits
31 Commits
electron-c
...
claimview-
| Author | SHA1 | Date | |
|---|---|---|---|
| 030960dd59 | |||
| b138441d10 | |||
| de45e83ffb | |||
| ba587471f9 | |||
| 2f05d27b51 | |||
| 40c8189c51 | |||
| cd7755979f | |||
| 4fa8c8f4cb | |||
|
|
1eeb013638 | ||
|
|
3e5e2cd0bb | ||
|
|
d87f44b75d | ||
| 2c7cb9333e | |||
| fa8956fb38 | |||
|
|
1499211018 | ||
|
|
25e37cc415 | ||
|
|
d339f1a274 | ||
|
|
c2e7531554 | ||
| aa64f426f3 | |||
|
|
c9082fa57b | ||
|
|
e67c97821a | ||
|
|
40fa38a9ce | ||
|
|
96e4d3c394 | ||
|
|
c4f2bb5e3a | ||
|
|
f51408e32a | ||
| 528a68ef6c | |||
| 8991b36a56 | |||
| 6f5661d61c | |||
|
|
e5ad71505c | ||
| 19f0c270d3 | |||
|
|
693173f09d | ||
|
|
a1388539c1 |
@@ -1689,3 +1689,11 @@ export const NOTIFY_CONTACTS_ADDED_CONFIRM = {
|
|||||||
title: "They're Added To Your List",
|
title: "They're Added To Your List",
|
||||||
message: "Would you like to go to the main page now?",
|
message: "Would you like to go to the main page now?",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ImportAccountView.vue specific constants
|
||||||
|
// Used in: ImportAccountView.vue (onImportClick method - duplicate account warning)
|
||||||
|
export const NOTIFY_DUPLICATE_ACCOUNT_IMPORT = {
|
||||||
|
title: "Account Already Imported",
|
||||||
|
message:
|
||||||
|
"This account has already been imported. Please use a different seed phrase or check your existing accounts.",
|
||||||
|
};
|
||||||
|
|||||||
@@ -1313,6 +1313,28 @@ export const capitalizeAndInsertSpacesBeforeCaps = (text: string) => {
|
|||||||
: text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
|
: text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats type string for display by adding spaces before capitals
|
||||||
|
* and optionally adds an appropriate article prefix (a/an)
|
||||||
|
*
|
||||||
|
* @param text - Text to format
|
||||||
|
* @returns Formatted string with article prefix
|
||||||
|
*/
|
||||||
|
export const capitalizeAndInsertSpacesBeforeCapsWithAPrefix = (
|
||||||
|
text: string,
|
||||||
|
): string => {
|
||||||
|
const word = capitalizeAndInsertSpacesBeforeCaps(text);
|
||||||
|
if (word) {
|
||||||
|
// if the word starts with a vowel, use "an" instead of "a"
|
||||||
|
const firstLetter = word[0].toLowerCase();
|
||||||
|
const vowels = ["a", "e", "i", "o", "u"];
|
||||||
|
const particle = vowels.includes(firstLetter) ? "an" : "a";
|
||||||
|
return particle + " " + word;
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
return readable summary of claim, or something generic
|
return readable summary of claim, or something generic
|
||||||
|
|
||||||
|
|||||||
175
src/libs/util.ts
175
src/libs/util.ts
@@ -160,6 +160,41 @@ export const isGiveAction = (
|
|||||||
return isGiveClaimType(veriClaim.claimType);
|
return isGiveClaimType(veriClaim.claimType);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface OfferFulfillment {
|
||||||
|
offerHandleId: string;
|
||||||
|
offerType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract offer fulfillment information from the fulfills field
|
||||||
|
* Handles both array and single object cases
|
||||||
|
*/
|
||||||
|
export const extractOfferFulfillment = (fulfills: any): OfferFulfillment | null => {
|
||||||
|
if (!fulfills) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle both array and single object cases
|
||||||
|
let offerFulfill = null;
|
||||||
|
|
||||||
|
if (Array.isArray(fulfills)) {
|
||||||
|
// Find the Offer in the fulfills array
|
||||||
|
offerFulfill = fulfills.find((item) => item["@type"] === "Offer");
|
||||||
|
} else if (fulfills["@type"] === "Offer") {
|
||||||
|
// fulfills is a single Offer object
|
||||||
|
offerFulfill = fulfills;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offerFulfill) {
|
||||||
|
return {
|
||||||
|
offerHandleId: offerFulfill.identifier,
|
||||||
|
offerType: offerFulfill["@type"],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
export const shortDid = (did: string) => {
|
export const shortDid = (did: string) => {
|
||||||
if (did.startsWith("did:peer:")) {
|
if (did.startsWith("did:peer:")) {
|
||||||
return (
|
return (
|
||||||
@@ -614,57 +649,64 @@ export const retrieveAllAccountsMetadata = async (): Promise<
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DUPLICATE_ACCOUNT_ERROR = "Cannot import duplicate account.";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves a new identity to both SQL and Dexie databases
|
* Saves a new identity to SQL database
|
||||||
*/
|
*/
|
||||||
export async function saveNewIdentity(
|
export async function saveNewIdentity(
|
||||||
identity: IIdentifier,
|
identity: IIdentifier,
|
||||||
mnemonic: string,
|
mnemonic: string,
|
||||||
derivationPath: string,
|
derivationPath: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
// add to the new sql db
|
||||||
// add to the new sql db
|
const platformService = await getPlatformService();
|
||||||
const platformService = await getPlatformService();
|
|
||||||
|
|
||||||
const secrets = await platformService.dbQuery(
|
// Check if account already exists before attempting to save
|
||||||
`SELECT secretBase64 FROM secret`,
|
const existingAccount = await platformService.dbQuery(
|
||||||
);
|
"SELECT did FROM accounts WHERE did = ?",
|
||||||
if (!secrets?.values?.length || !secrets.values[0]?.length) {
|
[identity.did],
|
||||||
throw new Error(
|
);
|
||||||
"No initial encryption supported. We recommend you clear your data and start over.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const secretBase64 = secrets.values[0][0] as string;
|
if (existingAccount?.values?.length) {
|
||||||
|
|
||||||
const secret = base64ToArrayBuffer(secretBase64);
|
|
||||||
const identityStr = JSON.stringify(identity);
|
|
||||||
const encryptedIdentity = await simpleEncrypt(identityStr, secret);
|
|
||||||
const encryptedMnemonic = await simpleEncrypt(mnemonic, secret);
|
|
||||||
const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity);
|
|
||||||
const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic);
|
|
||||||
|
|
||||||
const sql = `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?)`;
|
|
||||||
const params = [
|
|
||||||
new Date().toISOString(),
|
|
||||||
derivationPath,
|
|
||||||
identity.did,
|
|
||||||
encryptedIdentityBase64,
|
|
||||||
encryptedMnemonicBase64,
|
|
||||||
identity.keys[0].publicKeyHex,
|
|
||||||
];
|
|
||||||
await platformService.dbExec(sql, params);
|
|
||||||
|
|
||||||
await platformService.updateDefaultSettings({ activeDid: identity.did });
|
|
||||||
|
|
||||||
await platformService.insertNewDidIntoSettings(identity.did);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Failed to update default settings:", error);
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Failed to set default settings. Please try again or restart the app.",
|
`Account with DID ${identity.did} already exists. ${DUPLICATE_ACCOUNT_ERROR}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const secrets = await platformService.dbQuery(
|
||||||
|
`SELECT secretBase64 FROM secret`,
|
||||||
|
);
|
||||||
|
if (!secrets?.values?.length || !secrets.values[0]?.length) {
|
||||||
|
throw new Error(
|
||||||
|
"No initial encryption supported. We recommend you clear your data and start over.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const secretBase64 = secrets.values[0][0] as string;
|
||||||
|
|
||||||
|
const secret = base64ToArrayBuffer(secretBase64);
|
||||||
|
const identityStr = JSON.stringify(identity);
|
||||||
|
const encryptedIdentity = await simpleEncrypt(identityStr, secret);
|
||||||
|
const encryptedMnemonic = await simpleEncrypt(mnemonic, secret);
|
||||||
|
const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity);
|
||||||
|
const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic);
|
||||||
|
|
||||||
|
const sql = `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)`;
|
||||||
|
const params = [
|
||||||
|
new Date().toISOString(),
|
||||||
|
derivationPath,
|
||||||
|
identity.did,
|
||||||
|
encryptedIdentityBase64,
|
||||||
|
encryptedMnemonicBase64,
|
||||||
|
identity.keys[0].publicKeyHex,
|
||||||
|
];
|
||||||
|
await platformService.dbExec(sql, params);
|
||||||
|
|
||||||
|
await platformService.updateDefaultSettings({ activeDid: identity.did });
|
||||||
|
|
||||||
|
await platformService.insertNewDidIntoSettings(identity.did);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1032,3 +1074,58 @@ export async function importFromMnemonic(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an account with the given DID already exists in the database
|
||||||
|
*
|
||||||
|
* @param did - The DID to check for duplicates
|
||||||
|
* @returns Promise<boolean> - True if account already exists, false otherwise
|
||||||
|
* @throws Error if database query fails
|
||||||
|
*/
|
||||||
|
export async function checkForDuplicateAccount(did: string): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an account with the given DID already exists in the database
|
||||||
|
*
|
||||||
|
* @param mnemonic - The mnemonic phrase to derive DID from
|
||||||
|
* @param derivationPath - The derivation path to use
|
||||||
|
* @returns Promise<boolean> - True if account already exists, false otherwise
|
||||||
|
* @throws Error if database query fails
|
||||||
|
*/
|
||||||
|
export async function checkForDuplicateAccount(
|
||||||
|
mnemonic: string,
|
||||||
|
derivationPath: string,
|
||||||
|
): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of checkForDuplicateAccount with overloaded signatures
|
||||||
|
*/
|
||||||
|
export async function checkForDuplicateAccount(
|
||||||
|
didOrMnemonic: string,
|
||||||
|
derivationPath?: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
let didToCheck: string;
|
||||||
|
|
||||||
|
if (derivationPath) {
|
||||||
|
// Derive the DID from mnemonic and derivation path
|
||||||
|
const [address, privateHex, publicHex] = deriveAddress(
|
||||||
|
didOrMnemonic.trim().toLowerCase(),
|
||||||
|
derivationPath,
|
||||||
|
);
|
||||||
|
|
||||||
|
const newId = newIdentifier(address, privateHex, publicHex, derivationPath);
|
||||||
|
didToCheck = newId.did;
|
||||||
|
} else {
|
||||||
|
// Use the provided DID directly
|
||||||
|
didToCheck = didOrMnemonic;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if an account with this DID already exists
|
||||||
|
const platformService = await getPlatformService();
|
||||||
|
const existingAccount = await platformService.dbQuery(
|
||||||
|
"SELECT did FROM accounts WHERE did = ?",
|
||||||
|
[didToCheck],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (existingAccount?.values?.length ?? 0) > 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
// Generate a short random ID for this scanner instance
|
// Generate a short random ID for this scanner instance
|
||||||
this.id = Math.random().toString(36).substring(2, 8).toUpperCase();
|
this.id = Math.random().toString(36).substring(2, 8).toUpperCase();
|
||||||
this.options = options ?? {};
|
this.options = options ?? {};
|
||||||
logger.error(
|
logger.debug(
|
||||||
`[WebInlineQRScanner:${this.id}] Initializing scanner with options:`,
|
`[WebInlineQRScanner:${this.id}] Initializing scanner with options:`,
|
||||||
{
|
{
|
||||||
...this.options,
|
...this.options,
|
||||||
@@ -49,7 +49,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
this.context = this.canvas.getContext("2d", { willReadFrequently: true });
|
this.context = this.canvas.getContext("2d", { willReadFrequently: true });
|
||||||
this.video = document.createElement("video");
|
this.video = document.createElement("video");
|
||||||
this.video.setAttribute("playsinline", "true"); // Required for iOS
|
this.video.setAttribute("playsinline", "true"); // Required for iOS
|
||||||
logger.error(
|
logger.debug(
|
||||||
`[WebInlineQRScanner:${this.id}] DOM elements created successfully`,
|
`[WebInlineQRScanner:${this.id}] DOM elements created successfully`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -60,7 +60,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
this.cameraStateListeners.forEach((listener) => {
|
this.cameraStateListeners.forEach((listener) => {
|
||||||
try {
|
try {
|
||||||
listener.onStateChange(state, message);
|
listener.onStateChange(state, message);
|
||||||
logger.info(
|
logger.debug(
|
||||||
`[WebInlineQRScanner:${this.id}] Camera state changed to: ${state}`,
|
`[WebInlineQRScanner:${this.id}] Camera state changed to: ${state}`,
|
||||||
{
|
{
|
||||||
state,
|
state,
|
||||||
@@ -89,7 +89,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
async checkPermissions(): Promise<boolean> {
|
async checkPermissions(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
this.updateCameraState("initializing", "Checking camera permissions...");
|
this.updateCameraState("initializing", "Checking camera permissions...");
|
||||||
logger.error(
|
logger.debug(
|
||||||
`[WebInlineQRScanner:${this.id}] Checking camera permissions...`,
|
`[WebInlineQRScanner:${this.id}] Checking camera permissions...`,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
const permissions = await navigator.permissions.query({
|
const permissions = await navigator.permissions.query({
|
||||||
name: "camera" as PermissionName,
|
name: "camera" as PermissionName,
|
||||||
});
|
});
|
||||||
logger.error(
|
logger.debug(
|
||||||
`[WebInlineQRScanner:${this.id}] Permission state from Permissions API:`,
|
`[WebInlineQRScanner:${this.id}] Permission state from Permissions API:`,
|
||||||
permissions.state,
|
permissions.state,
|
||||||
);
|
);
|
||||||
@@ -165,7 +165,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
"initializing",
|
"initializing",
|
||||||
"Requesting camera permissions...",
|
"Requesting camera permissions...",
|
||||||
);
|
);
|
||||||
logger.error(
|
logger.debug(
|
||||||
`[WebInlineQRScanner:${this.id}] Requesting camera permissions...`,
|
`[WebInlineQRScanner:${this.id}] Requesting camera permissions...`,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
(device) => device.kind === "videoinput",
|
(device) => device.kind === "videoinput",
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.error(`[WebInlineQRScanner:${this.id}] Found video devices:`, {
|
logger.debug(`[WebInlineQRScanner:${this.id}] Found video devices:`, {
|
||||||
count: videoDevices.length,
|
count: videoDevices.length,
|
||||||
devices: videoDevices.map((d) => ({ id: d.deviceId, label: d.label })),
|
devices: videoDevices.map((d) => ({ id: d.deviceId, label: d.label })),
|
||||||
userAgent: navigator.userAgent,
|
userAgent: navigator.userAgent,
|
||||||
@@ -188,7 +188,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try to get a stream with specific constraints
|
// Try to get a stream with specific constraints
|
||||||
logger.error(
|
logger.debug(
|
||||||
`[WebInlineQRScanner:${this.id}] Requesting camera stream with constraints:`,
|
`[WebInlineQRScanner:${this.id}] Requesting camera stream with constraints:`,
|
||||||
{
|
{
|
||||||
facingMode: "environment",
|
facingMode: "environment",
|
||||||
@@ -210,7 +210,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
|
|
||||||
// Stop the test stream immediately
|
// Stop the test stream immediately
|
||||||
stream.getTracks().forEach((track) => {
|
stream.getTracks().forEach((track) => {
|
||||||
logger.error(`[WebInlineQRScanner:${this.id}] Stopping test track:`, {
|
logger.debug(`[WebInlineQRScanner:${this.id}] Stopping test track:`, {
|
||||||
kind: track.kind,
|
kind: track.kind,
|
||||||
label: track.label,
|
label: track.label,
|
||||||
readyState: track.readyState,
|
readyState: track.readyState,
|
||||||
@@ -275,12 +275,12 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
|
|
||||||
async isSupported(): Promise<boolean> {
|
async isSupported(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
logger.error(
|
logger.debug(
|
||||||
`[WebInlineQRScanner:${this.id}] Checking browser support...`,
|
`[WebInlineQRScanner:${this.id}] Checking browser support...`,
|
||||||
);
|
);
|
||||||
// Check for secure context first
|
// Check for secure context first
|
||||||
if (!window.isSecureContext) {
|
if (!window.isSecureContext) {
|
||||||
logger.error(
|
logger.debug(
|
||||||
`[WebInlineQRScanner:${this.id}] Camera access requires HTTPS (secure context)`,
|
`[WebInlineQRScanner:${this.id}] Camera access requires HTTPS (secure context)`,
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
@@ -300,7 +300,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
(device) => device.kind === "videoinput",
|
(device) => device.kind === "videoinput",
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.error(`[WebInlineQRScanner:${this.id}] Device support check:`, {
|
logger.debug(`[WebInlineQRScanner:${this.id}] Device support check:`, {
|
||||||
hasSecureContext: window.isSecureContext,
|
hasSecureContext: window.isSecureContext,
|
||||||
hasMediaDevices: !!navigator.mediaDevices,
|
hasMediaDevices: !!navigator.mediaDevices,
|
||||||
hasGetUserMedia: !!navigator.mediaDevices?.getUserMedia,
|
hasGetUserMedia: !!navigator.mediaDevices?.getUserMedia,
|
||||||
@@ -379,7 +379,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
|
|
||||||
// Log scan attempt every 100 frames or 1 second
|
// Log scan attempt every 100 frames or 1 second
|
||||||
if (this.scanAttempts % 100 === 0 || timeSinceLastScan >= 1000) {
|
if (this.scanAttempts % 100 === 0 || timeSinceLastScan >= 1000) {
|
||||||
logger.error(`[WebInlineQRScanner:${this.id}] Scanning frame:`, {
|
logger.debug(`[WebInlineQRScanner:${this.id}] Scanning frame:`, {
|
||||||
attempt: this.scanAttempts,
|
attempt: this.scanAttempts,
|
||||||
dimensions: {
|
dimensions: {
|
||||||
width: this.canvas.width,
|
width: this.canvas.width,
|
||||||
@@ -421,7 +421,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
!code.data ||
|
!code.data ||
|
||||||
code.data.length === 0;
|
code.data.length === 0;
|
||||||
|
|
||||||
logger.error(`[WebInlineQRScanner:${this.id}] QR Code detected:`, {
|
logger.debug(`[WebInlineQRScanner:${this.id}] QR Code detected:`, {
|
||||||
data: code.data,
|
data: code.data,
|
||||||
location: code.location,
|
location: code.location,
|
||||||
attempts: this.scanAttempts,
|
attempts: this.scanAttempts,
|
||||||
@@ -512,13 +512,13 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
this.scanAttempts = 0;
|
this.scanAttempts = 0;
|
||||||
this.lastScanTime = Date.now();
|
this.lastScanTime = Date.now();
|
||||||
this.updateCameraState("initializing", "Starting camera...");
|
this.updateCameraState("initializing", "Starting camera...");
|
||||||
logger.error(
|
logger.debug(
|
||||||
`[WebInlineQRScanner:${this.id}] Starting scan with options:`,
|
`[WebInlineQRScanner:${this.id}] Starting scan with options:`,
|
||||||
this.options,
|
this.options,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get camera stream with options
|
// Get camera stream with options
|
||||||
logger.error(
|
logger.debug(
|
||||||
`[WebInlineQRScanner:${this.id}] Requesting camera stream...`,
|
`[WebInlineQRScanner:${this.id}] Requesting camera stream...`,
|
||||||
);
|
);
|
||||||
this.stream = await navigator.mediaDevices.getUserMedia({
|
this.stream = await navigator.mediaDevices.getUserMedia({
|
||||||
@@ -531,7 +531,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
|
|
||||||
this.updateCameraState("active", "Camera is active");
|
this.updateCameraState("active", "Camera is active");
|
||||||
|
|
||||||
logger.error(`[WebInlineQRScanner:${this.id}] Camera stream obtained:`, {
|
logger.debug(`[WebInlineQRScanner:${this.id}] Camera stream obtained:`, {
|
||||||
tracks: this.stream.getTracks().map((t) => ({
|
tracks: this.stream.getTracks().map((t) => ({
|
||||||
kind: t.kind,
|
kind: t.kind,
|
||||||
label: t.label,
|
label: t.label,
|
||||||
@@ -550,14 +550,14 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
this.video.style.display = "none";
|
this.video.style.display = "none";
|
||||||
}
|
}
|
||||||
await this.video.play();
|
await this.video.play();
|
||||||
logger.error(
|
logger.debug(
|
||||||
`[WebInlineQRScanner:${this.id}] Video element started playing`,
|
`[WebInlineQRScanner:${this.id}] Video element started playing`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit stream to component
|
// Emit stream to component
|
||||||
this.events.emit("stream", this.stream);
|
this.events.emit("stream", this.stream);
|
||||||
logger.error(`[WebInlineQRScanner:${this.id}] Stream event emitted`);
|
logger.debug(`[WebInlineQRScanner:${this.id}] Stream event emitted`);
|
||||||
|
|
||||||
// Start QR code scanning
|
// Start QR code scanning
|
||||||
this.scanQRCode();
|
this.scanQRCode();
|
||||||
@@ -595,7 +595,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.error(`[WebInlineQRScanner:${this.id}] Stopping scan`, {
|
logger.debug(`[WebInlineQRScanner:${this.id}] Stopping scan`, {
|
||||||
scanAttempts: this.scanAttempts,
|
scanAttempts: this.scanAttempts,
|
||||||
duration: Date.now() - this.lastScanTime,
|
duration: Date.now() - this.lastScanTime,
|
||||||
});
|
});
|
||||||
@@ -604,7 +604,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
if (this.animationFrameId !== null) {
|
if (this.animationFrameId !== null) {
|
||||||
cancelAnimationFrame(this.animationFrameId);
|
cancelAnimationFrame(this.animationFrameId);
|
||||||
this.animationFrameId = null;
|
this.animationFrameId = null;
|
||||||
logger.error(
|
logger.debug(
|
||||||
`[WebInlineQRScanner:${this.id}] Animation frame cancelled`,
|
`[WebInlineQRScanner:${this.id}] Animation frame cancelled`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -613,13 +613,13 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
if (this.video) {
|
if (this.video) {
|
||||||
this.video.pause();
|
this.video.pause();
|
||||||
this.video.srcObject = null;
|
this.video.srcObject = null;
|
||||||
logger.error(`[WebInlineQRScanner:${this.id}] Video element stopped`);
|
logger.debug(`[WebInlineQRScanner:${this.id}] Video element stopped`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop all tracks in the stream
|
// Stop all tracks in the stream
|
||||||
if (this.stream) {
|
if (this.stream) {
|
||||||
this.stream.getTracks().forEach((track) => {
|
this.stream.getTracks().forEach((track) => {
|
||||||
logger.error(`[WebInlineQRScanner:${this.id}] Stopping track:`, {
|
logger.debug(`[WebInlineQRScanner:${this.id}] Stopping track:`, {
|
||||||
kind: track.kind,
|
kind: track.kind,
|
||||||
label: track.label,
|
label: track.label,
|
||||||
readyState: track.readyState,
|
readyState: track.readyState,
|
||||||
@@ -631,7 +631,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
|
|
||||||
// Emit stream stopped event
|
// Emit stream stopped event
|
||||||
this.events.emit("stream", null);
|
this.events.emit("stream", null);
|
||||||
logger.error(
|
logger.debug(
|
||||||
`[WebInlineQRScanner:${this.id}] Stream stopped event emitted`,
|
`[WebInlineQRScanner:${this.id}] Stream stopped event emitted`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -643,17 +643,17 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
this.isScanning = false;
|
this.isScanning = false;
|
||||||
logger.error(`[WebInlineQRScanner:${this.id}] Scan stopped successfully`);
|
logger.debug(`[WebInlineQRScanner:${this.id}] Scan stopped successfully`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addListener(listener: ScanListener): void {
|
addListener(listener: ScanListener): void {
|
||||||
logger.error(`[WebInlineQRScanner:${this.id}] Adding scan listener`);
|
logger.debug(`[WebInlineQRScanner:${this.id}] Adding scan listener`);
|
||||||
this.scanListener = listener;
|
this.scanListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
onStream(callback: (stream: MediaStream | null) => void): void {
|
onStream(callback: (stream: MediaStream | null) => void): void {
|
||||||
logger.error(
|
logger.debug(
|
||||||
`[WebInlineQRScanner:${this.id}] Adding stream event listener`,
|
`[WebInlineQRScanner:${this.id}] Adding stream event listener`,
|
||||||
);
|
);
|
||||||
this.events.on("stream", callback);
|
this.events.on("stream", callback);
|
||||||
@@ -661,24 +661,24 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
|
|
||||||
async cleanup(): Promise<void> {
|
async cleanup(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
logger.error(`[WebInlineQRScanner:${this.id}] Starting cleanup`);
|
logger.debug(`[WebInlineQRScanner:${this.id}] Starting cleanup`);
|
||||||
await this.stopScan();
|
await this.stopScan();
|
||||||
this.events.removeAllListeners();
|
this.events.removeAllListeners();
|
||||||
logger.error(`[WebInlineQRScanner:${this.id}] Event listeners removed`);
|
logger.debug(`[WebInlineQRScanner:${this.id}] Event listeners removed`);
|
||||||
|
|
||||||
// Clean up DOM elements
|
// Clean up DOM elements
|
||||||
if (this.video) {
|
if (this.video) {
|
||||||
this.video.remove();
|
this.video.remove();
|
||||||
this.video = null;
|
this.video = null;
|
||||||
logger.error(`[WebInlineQRScanner:${this.id}] Video element removed`);
|
logger.debug(`[WebInlineQRScanner:${this.id}] Video element removed`);
|
||||||
}
|
}
|
||||||
if (this.canvas) {
|
if (this.canvas) {
|
||||||
this.canvas.remove();
|
this.canvas.remove();
|
||||||
this.canvas = null;
|
this.canvas = null;
|
||||||
logger.error(`[WebInlineQRScanner:${this.id}] Canvas element removed`);
|
logger.debug(`[WebInlineQRScanner:${this.id}] Canvas element removed`);
|
||||||
}
|
}
|
||||||
this.context = null;
|
this.context = null;
|
||||||
logger.error(
|
logger.debug(
|
||||||
`[WebInlineQRScanner:${this.id}] Cleanup completed successfully`,
|
`[WebInlineQRScanner:${this.id}] Cleanup completed successfully`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -24,7 +24,9 @@
|
|||||||
<div class="flex columns-3">
|
<div class="flex columns-3">
|
||||||
<h2 class="text-md font-bold w-full">
|
<h2 class="text-md font-bold w-full">
|
||||||
{{
|
{{
|
||||||
capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType || "")
|
serverUtil.capitalizeAndInsertSpacesBeforeCaps(
|
||||||
|
veriClaim.claimType || "",
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
<button
|
<button
|
||||||
v-if="canEditClaim"
|
v-if="canEditClaim"
|
||||||
@@ -106,77 +108,91 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Fullfills Links -->
|
<!-- Fullfills Links -->
|
||||||
|
<div class="mt-4 empty:hidden">
|
||||||
<!-- fullfills links for a give -->
|
<!-- fullfills links for a give -->
|
||||||
<div v-if="detailsForGive?.fulfillsPlanHandleId" class="mt-4">
|
<div v-if="detailsForGive?.fulfillsPlanHandleId">
|
||||||
<router-link
|
<router-link
|
||||||
:to="
|
:to="
|
||||||
'/project/' +
|
'/project/' +
|
||||||
encodeURIComponent(detailsForGive?.fulfillsPlanHandleId)
|
encodeURIComponent(detailsForGive?.fulfillsPlanHandleId)
|
||||||
"
|
"
|
||||||
class="text-blue-500 mt-2"
|
class="text-blue-500 mt-2"
|
||||||
>
|
|
||||||
Fulfills a bigger plan...
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
<!-- if there's another, it's probably fulfilling an offer, too -->
|
|
||||||
<div
|
|
||||||
v-if="
|
|
||||||
detailsForGive?.fulfillsType &&
|
|
||||||
detailsForGive?.fulfillsType !== 'PlanAction' &&
|
|
||||||
detailsForGive?.fulfillsHandleId
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<!-- router-link to /claim/ only changes URL path -->
|
|
||||||
<a
|
|
||||||
class="text-blue-500 mt-4 cursor-pointer"
|
|
||||||
@click="
|
|
||||||
showDifferentClaimPage(detailsForGive?.fulfillsHandleId)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Fulfills
|
|
||||||
{{
|
|
||||||
capitalizeAndInsertSpacesBeforeCaps(
|
|
||||||
detailsForGive.fulfillsType,
|
|
||||||
)
|
|
||||||
}}...
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- fullfills links for an offer -->
|
|
||||||
<div v-if="detailsForOffer?.fulfillsPlanHandleId">
|
|
||||||
<router-link
|
|
||||||
:to="
|
|
||||||
'/project/' +
|
|
||||||
encodeURIComponent(detailsForOffer?.fulfillsPlanHandleId)
|
|
||||||
"
|
|
||||||
class="text-blue-500 mt-4"
|
|
||||||
>
|
|
||||||
Offered to a bigger plan...
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Providers -->
|
|
||||||
<div v-if="providersForGive?.length > 0" class="mt-4">
|
|
||||||
<span>Other assistance provided by:</span>
|
|
||||||
<ul class="ml-4">
|
|
||||||
<li
|
|
||||||
v-for="provider of providersForGive"
|
|
||||||
:key="provider.identifier"
|
|
||||||
class="list-disc ml-4"
|
|
||||||
>
|
>
|
||||||
<div class="flex gap-4">
|
This fulfills a bigger plan
|
||||||
<div class="grow overflow-hidden">
|
<font-awesome
|
||||||
<a
|
icon="arrow-up-right-from-square"
|
||||||
class="text-blue-500 mt-4 cursor-pointer"
|
class="fa-fw"
|
||||||
@click="handleProviderClick(provider)"
|
/>
|
||||||
>
|
</router-link>
|
||||||
an activity...
|
</div>
|
||||||
</a>
|
|
||||||
|
<!-- Show offer fulfillment if this give fulfills an offer -->
|
||||||
|
<div v-if="detailsForGiveOfferFulfillment?.offerHandleId">
|
||||||
|
<!-- router-link to /claim/ only changes URL path -->
|
||||||
|
<a
|
||||||
|
class="text-blue-500 mt-4 cursor-pointer"
|
||||||
|
@click="
|
||||||
|
showDifferentClaimPage(
|
||||||
|
detailsForGiveOfferFulfillment.offerHandleId,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
This fulfills
|
||||||
|
{{
|
||||||
|
serverUtil.capitalizeAndInsertSpacesBeforeCapsWithAPrefix(
|
||||||
|
detailsForGiveOfferFulfillment.offerType || "Offer",
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
<font-awesome
|
||||||
|
icon="arrow-up-right-from-square"
|
||||||
|
class="fa-fw"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- fullfills links for an offer -->
|
||||||
|
<div v-if="detailsForOffer?.fulfillsPlanHandleId">
|
||||||
|
<router-link
|
||||||
|
:to="
|
||||||
|
'/project/' +
|
||||||
|
encodeURIComponent(detailsForOffer?.fulfillsPlanHandleId)
|
||||||
|
"
|
||||||
|
class="text-blue-500 mt-4"
|
||||||
|
>
|
||||||
|
Offered to a bigger plan
|
||||||
|
<font-awesome
|
||||||
|
icon="arrow-up-right-from-square"
|
||||||
|
class="fa-fw"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Providers -->
|
||||||
|
<div v-if="providersForGive?.length > 0">
|
||||||
|
<span>Other assistance provided by:</span>
|
||||||
|
<ul class="ml-4">
|
||||||
|
<li
|
||||||
|
v-for="provider of providersForGive"
|
||||||
|
:key="provider.identifier"
|
||||||
|
class="list-disc ml-4"
|
||||||
|
>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div class="grow overflow-hidden">
|
||||||
|
<a
|
||||||
|
class="text-blue-500 mt-4 cursor-pointer"
|
||||||
|
@click="handleProviderClick(provider)"
|
||||||
|
>
|
||||||
|
an activity
|
||||||
|
<font-awesome
|
||||||
|
icon="arrow-up-right-from-square"
|
||||||
|
class="fa-fw"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
</ul>
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -556,6 +572,17 @@ export default class ClaimView extends Vue {
|
|||||||
fulfillsPlanHandleId?: string;
|
fulfillsPlanHandleId?: string;
|
||||||
fulfillsType?: string;
|
fulfillsType?: string;
|
||||||
fulfillsHandleId?: string;
|
fulfillsHandleId?: string;
|
||||||
|
fullClaim?: {
|
||||||
|
fulfills?: Array<{
|
||||||
|
"@type": string;
|
||||||
|
identifier?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
} | null = null;
|
||||||
|
// Additional offer information extracted from the fulfills array
|
||||||
|
detailsForGiveOfferFulfillment: {
|
||||||
|
offerHandleId?: string;
|
||||||
|
offerType?: string;
|
||||||
} | null = null;
|
} | null = null;
|
||||||
detailsForOffer: { fulfillsPlanHandleId?: string } | null = null;
|
detailsForOffer: { fulfillsPlanHandleId?: string } | null = null;
|
||||||
// Project information for fulfillsPlanHandleId
|
// Project information for fulfillsPlanHandleId
|
||||||
@@ -689,6 +716,7 @@ export default class ClaimView extends Vue {
|
|||||||
this.confsVisibleToIdList = [];
|
this.confsVisibleToIdList = [];
|
||||||
this.detailsForGive = null;
|
this.detailsForGive = null;
|
||||||
this.detailsForOffer = null;
|
this.detailsForOffer = null;
|
||||||
|
this.detailsForGiveOfferFulfillment = null;
|
||||||
this.projectInfo = null;
|
this.projectInfo = null;
|
||||||
this.fullClaim = null;
|
this.fullClaim = null;
|
||||||
this.fullClaimDump = "";
|
this.fullClaimDump = "";
|
||||||
@@ -701,6 +729,15 @@ export default class ClaimView extends Vue {
|
|||||||
this.veriClaimDidsVisible = {};
|
this.veriClaimDidsVisible = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract offer fulfillment information from the fulfills array
|
||||||
|
*/
|
||||||
|
extractOfferFulfillment() {
|
||||||
|
this.detailsForGiveOfferFulfillment = libsUtil.extractOfferFulfillment(
|
||||||
|
this.detailsForGive?.fullClaim?.fulfills
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// =================================================
|
// =================================================
|
||||||
// UTILITY METHODS
|
// UTILITY METHODS
|
||||||
// =================================================
|
// =================================================
|
||||||
@@ -758,13 +795,6 @@ export default class ClaimView extends Vue {
|
|||||||
this.canShare = !!navigator.share;
|
this.canShare = !!navigator.share;
|
||||||
}
|
}
|
||||||
|
|
||||||
// insert a space before any capital letters except the initial letter
|
|
||||||
// (and capitalize initial letter, just in case)
|
|
||||||
capitalizeAndInsertSpacesBeforeCaps(text: string): string {
|
|
||||||
if (!text) return "";
|
|
||||||
return text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
|
|
||||||
}
|
|
||||||
|
|
||||||
totalConfirmers() {
|
totalConfirmers() {
|
||||||
return (
|
return (
|
||||||
this.numConfsNotVisible +
|
this.numConfsNotVisible +
|
||||||
@@ -821,6 +851,8 @@ export default class ClaimView extends Vue {
|
|||||||
});
|
});
|
||||||
if (giveResp.status === 200 && giveResp.data.data?.length > 0) {
|
if (giveResp.status === 200 && giveResp.data.data?.length > 0) {
|
||||||
this.detailsForGive = giveResp.data.data[0];
|
this.detailsForGive = giveResp.data.data[0];
|
||||||
|
// Extract offer information from the fulfills array
|
||||||
|
this.extractOfferFulfillment();
|
||||||
} else {
|
} else {
|
||||||
await this.$logError(
|
await this.$logError(
|
||||||
"Error getting detailed give info: " + JSON.stringify(giveResp),
|
"Error getting detailed give info: " + JSON.stringify(giveResp),
|
||||||
|
|||||||
@@ -96,50 +96,50 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Fullfills Links -->
|
<!-- Fullfills Links -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<!-- fullfills links for a give -->
|
||||||
|
<div v-if="giveDetails?.fulfillsPlanHandleId">
|
||||||
|
<router-link
|
||||||
|
:to="
|
||||||
|
'/project/' +
|
||||||
|
encodeURIComponent(
|
||||||
|
giveDetails?.fulfillsPlanHandleId || '',
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="text-blue-500 mt-2 cursor-pointer"
|
||||||
|
>
|
||||||
|
This fulfills a bigger plan
|
||||||
|
<font-awesome
|
||||||
|
icon="arrow-up-right-from-square"
|
||||||
|
class="fa-fw"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- fullfills links for a give -->
|
<!-- Show offer fulfillment if this give fulfills an offer -->
|
||||||
<div v-if="giveDetails?.fulfillsPlanHandleId" class="mt-2">
|
<div v-if="giveDetailsOfferFulfillment?.offerHandleId">
|
||||||
<router-link
|
<!-- router-link to /claim/ only changes URL path -->
|
||||||
:to="
|
<router-link
|
||||||
'/project/' +
|
:to="
|
||||||
encodeURIComponent(giveDetails?.fulfillsPlanHandleId || '')
|
'/claim/' +
|
||||||
"
|
encodeURIComponent(
|
||||||
class="text-blue-500 mt-2 cursor-pointer"
|
giveDetailsOfferFulfillment.offerHandleId || '',
|
||||||
>
|
)
|
||||||
This fulfills a bigger plan
|
"
|
||||||
<font-awesome
|
class="text-blue-500 mt-2 cursor-pointer"
|
||||||
icon="arrow-up-right-from-square"
|
>
|
||||||
class="fa-fw"
|
This fulfills
|
||||||
/>
|
{{
|
||||||
</router-link>
|
serverUtil.capitalizeAndInsertSpacesBeforeCapsWithAPrefix(
|
||||||
</div>
|
giveDetailsOfferFulfillment.offerType || "Offer",
|
||||||
<!-- if there's another, it's probably fulfilling an offer, too -->
|
)
|
||||||
<div
|
}}
|
||||||
v-if="
|
<font-awesome
|
||||||
giveDetails?.fulfillsType &&
|
icon="arrow-up-right-from-square"
|
||||||
giveDetails?.fulfillsType !== 'PlanAction' &&
|
class="fa-fw"
|
||||||
giveDetails?.fulfillsHandleId
|
/>
|
||||||
"
|
</router-link>
|
||||||
>
|
</div>
|
||||||
<!-- router-link to /claim/ only changes URL path -->
|
|
||||||
<router-link
|
|
||||||
:to="
|
|
||||||
'/claim/' +
|
|
||||||
encodeURIComponent(giveDetails?.fulfillsHandleId || '')
|
|
||||||
"
|
|
||||||
class="text-blue-500 mt-2 cursor-pointer"
|
|
||||||
>
|
|
||||||
This fulfills
|
|
||||||
{{
|
|
||||||
capitalizeAndInsertSpacesBeforeCapsWithAPrefix(
|
|
||||||
giveDetails?.fulfillsType || "",
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
<font-awesome
|
|
||||||
icon="arrow-up-right-from-square"
|
|
||||||
class="fa-fw"
|
|
||||||
/>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -493,6 +493,11 @@ export default class ConfirmGiftView extends Vue {
|
|||||||
confsVisibleErrorMessage = "";
|
confsVisibleErrorMessage = "";
|
||||||
confsVisibleToIdList: string[] = []; // list of DIDs that can see any confirmer
|
confsVisibleToIdList: string[] = []; // list of DIDs that can see any confirmer
|
||||||
giveDetails?: GiveSummaryRecord;
|
giveDetails?: GiveSummaryRecord;
|
||||||
|
// Additional offer information extracted from the fulfills array
|
||||||
|
giveDetailsOfferFulfillment: {
|
||||||
|
offerHandleId?: string;
|
||||||
|
offerType?: string;
|
||||||
|
} | null = null;
|
||||||
giverName = "";
|
giverName = "";
|
||||||
issuerName = "";
|
issuerName = "";
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
@@ -648,6 +653,8 @@ export default class ConfirmGiftView extends Vue {
|
|||||||
|
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
this.giveDetails = resp.data.data[0];
|
this.giveDetails = resp.data.data[0];
|
||||||
|
// Extract offer information from the fulfills array
|
||||||
|
this.extractOfferFulfillment();
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Error getting detailed give info: " + resp.status);
|
throw new Error("Error getting detailed give info: " + resp.status);
|
||||||
}
|
}
|
||||||
@@ -707,6 +714,15 @@ export default class ConfirmGiftView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract offer fulfillment information from the fulfills array
|
||||||
|
*/
|
||||||
|
private extractOfferFulfillment() {
|
||||||
|
this.giveDetailsOfferFulfillment = libsUtil.extractOfferFulfillment(
|
||||||
|
this.giveDetails?.fullClaim?.fulfills
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches confirmer information for the claim
|
* Fetches confirmer information for the claim
|
||||||
*/
|
*/
|
||||||
@@ -849,27 +865,6 @@ export default class ConfirmGiftView extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats type string for display by adding spaces before capitals
|
|
||||||
* Optionally adds a prefix
|
|
||||||
*
|
|
||||||
* @param text - Text to format
|
|
||||||
* @param prefix - Optional prefix to add
|
|
||||||
* @returns Formatted string
|
|
||||||
*/
|
|
||||||
capitalizeAndInsertSpacesBeforeCapsWithAPrefix(text: string): string {
|
|
||||||
const word = this.capitalizeAndInsertSpacesBeforeCaps(text);
|
|
||||||
if (word) {
|
|
||||||
// if the word starts with a vowel, use "an" instead of "a"
|
|
||||||
const firstLetter = word[0].toLowerCase();
|
|
||||||
const vowels = ["a", "e", "i", "o", "u"];
|
|
||||||
const particle = vowels.includes(firstLetter) ? "an" : "a";
|
|
||||||
return particle + " " + word;
|
|
||||||
} else {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiates sharing of claim information
|
* Initiates sharing of claim information
|
||||||
* Handles share functionality based on platform capabilities
|
* Handles share functionality based on platform capabilities
|
||||||
@@ -894,11 +889,5 @@ export default class ConfirmGiftView extends Vue {
|
|||||||
this.veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
this.veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
||||||
this.veriClaimDump = "";
|
this.veriClaimDump = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
capitalizeAndInsertSpacesBeforeCaps(text: string) {
|
|
||||||
return !text
|
|
||||||
? ""
|
|
||||||
: text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -71,22 +71,22 @@
|
|||||||
contactFromDid?.seesMe && contactFromDid.did !== activeDid
|
contactFromDid?.seesMe && contactFromDid.did !== activeDid
|
||||||
"
|
"
|
||||||
class="text-sm 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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
class="text-sm 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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||||
title="They can see you"
|
title="They can see your activity"
|
||||||
@click="confirmSetVisibility(contactFromDid, false)"
|
@click="confirmSetVisibility(contactFromDid, false)"
|
||||||
>
|
>
|
||||||
<font-awesome icon="eye" class="fa-fw" />
|
|
||||||
<font-awesome icon="arrow-up" class="fa-fw" />
|
<font-awesome icon="arrow-up" class="fa-fw" />
|
||||||
|
<font-awesome icon="eye" class="fa-fw" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-else-if="
|
v-else-if="
|
||||||
!contactFromDid?.seesMe && contactFromDid?.did !== activeDid
|
!contactFromDid?.seesMe && contactFromDid?.did !== activeDid
|
||||||
"
|
"
|
||||||
class="text-sm 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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
class="text-sm 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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||||
title="They cannot see you"
|
title="They cannot see your activity"
|
||||||
@click="confirmSetVisibility(contactFromDid, true)"
|
@click="confirmSetVisibility(contactFromDid, true)"
|
||||||
>
|
>
|
||||||
<font-awesome icon="eye-slash" class="fa-fw" />
|
|
||||||
<font-awesome icon="arrow-up" class="fa-fw" />
|
<font-awesome icon="arrow-up" class="fa-fw" />
|
||||||
|
<font-awesome icon="eye-slash" class="fa-fw" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -95,11 +95,11 @@
|
|||||||
contactFromDid.did !== activeDid
|
contactFromDid.did !== activeDid
|
||||||
"
|
"
|
||||||
class="text-sm 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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
class="text-sm 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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||||
title="I view their content"
|
title="You watch their activity"
|
||||||
@click="confirmViewContent(contactFromDid, false)"
|
@click="confirmViewContent(contactFromDid, false)"
|
||||||
>
|
>
|
||||||
<font-awesome icon="eye" class="fa-fw" />
|
|
||||||
<font-awesome icon="arrow-down" class="fa-fw" />
|
<font-awesome icon="arrow-down" class="fa-fw" />
|
||||||
|
<font-awesome icon="eye" class="fa-fw" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-else-if="
|
v-else-if="
|
||||||
@@ -107,11 +107,11 @@
|
|||||||
contactFromDid?.did !== activeDid
|
contactFromDid?.did !== activeDid
|
||||||
"
|
"
|
||||||
class="text-sm 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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
class="text-sm 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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||||
title="I do not view their content"
|
title="You do not watch their activity"
|
||||||
@click="confirmViewContent(contactFromDid, true)"
|
@click="confirmViewContent(contactFromDid, true)"
|
||||||
>
|
>
|
||||||
<font-awesome icon="eye-slash" class="fa-fw" />
|
|
||||||
<font-awesome icon="arrow-down" class="fa-fw" />
|
<font-awesome icon="arrow-down" class="fa-fw" />
|
||||||
|
<font-awesome icon="eye-slash" class="fa-fw" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -319,8 +319,9 @@
|
|||||||
<ul class="list-disc list-outside ml-4">
|
<ul class="list-disc list-outside ml-4">
|
||||||
<li>
|
<li>
|
||||||
Go to Your Identity <font-awesome icon="circle-user" class="fa-fw" /> page,
|
Go to Your Identity <font-awesome icon="circle-user" class="fa-fw" /> page,
|
||||||
click Advanced, and follow the instructions for the Contacts & Settings Database "Import".
|
click Advanced, and follow the instructions to "Import Contacts".
|
||||||
Beware that this will erase your existing contact & settings.
|
(There is currently no way to import other settings, so you'll have to recreate
|
||||||
|
by hand your search area, filters, etc.)
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -336,14 +337,18 @@
|
|||||||
|
|
||||||
<h2 class="text-xl font-semibold">How do I erase my data from my device?</h2>
|
<h2 class="text-xl font-semibold">How do I erase my data from my device?</h2>
|
||||||
<p>
|
<p>
|
||||||
Before doing this, you may want to back up your data with the instructions above.
|
Before doing this, you should back up your data with the instructions above.
|
||||||
|
Note that this does not erase data sent to our servers (see contact info below)
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li class="list-disc list-outside ml-4">
|
<li class="list-disc list-outside ml-4">
|
||||||
Mobile
|
Mobile
|
||||||
<ul>
|
<ul>
|
||||||
<li class="list-disc list-outside ml-4">
|
<li class="list-disc list-outside ml-4">
|
||||||
Home Screen: hold down on the icon, and choose to delete it
|
App Store app: hold down on the icon, then uninstall it
|
||||||
|
</li>
|
||||||
|
<li class="list-disc list-outside ml-4">
|
||||||
|
Home Screen PWA: hold down on the icon, and delete it
|
||||||
</li>
|
</li>
|
||||||
<li class="list-disc list-outside ml-4">
|
<li class="list-disc list-outside ml-4">
|
||||||
Chrome: Settings -> Privacy and Security -> Clear Browsing Data
|
Chrome: Settings -> Privacy and Security -> Clear Browsing Data
|
||||||
@@ -415,15 +420,6 @@
|
|||||||
different page.
|
different page.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">
|
|
||||||
Where do I get help with notifications?
|
|
||||||
</h2>
|
|
||||||
<p>
|
|
||||||
<router-link class="text-blue-500" to="/help-notifications"
|
|
||||||
>Here.</router-link
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">
|
<h2 class="text-xl font-semibold">
|
||||||
This app is misbehaving, like showing me a blank screen or failing to show my personal data.
|
This app is misbehaving, like showing me a blank screen or failing to show my personal data.
|
||||||
What can I do?
|
What can I do?
|
||||||
@@ -434,10 +430,13 @@
|
|||||||
</p>
|
</p>
|
||||||
<ul class="list-disc list-outside ml-4">
|
<ul class="list-disc list-outside ml-4">
|
||||||
<li>
|
<li>
|
||||||
Drag down on the screen to refresh it; do that multiple times, because
|
For mobile apps, make sure you're connected to the internet.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
For PWAs, drag down on the screen to refresh it; do that multiple times, because
|
||||||
it sometimes takes multiple tries for the app to refresh to the latest version.
|
it sometimes takes multiple tries for the app to refresh to the latest version.
|
||||||
You can see the version information at the bottom of this page; the best
|
You can see the version information at the bottom of this page; the best
|
||||||
way to determine the latest version is to open this page in an incognito/private
|
way to determine the latest version is to open TimeSafari.app in an incognito/private
|
||||||
browser window and look at the version there.
|
browser window and look at the version there.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@@ -468,9 +467,6 @@
|
|||||||
</ul>
|
</ul>
|
||||||
Then reload Time Safari.
|
Then reload Time Safari.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
Restart your device.
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
If you still have problems, you can clear the cache (see "erase my data" above)
|
If you still have problems, you can clear the cache (see "erase my data" above)
|
||||||
@@ -508,16 +504,12 @@
|
|||||||
</p>
|
</p>
|
||||||
<ul class="list-disc list-outside ml-4">
|
<ul class="list-disc list-outside ml-4">
|
||||||
<li>
|
<li>
|
||||||
If using notifications, a server stores push token data. That can be revoked at any time
|
If sending images, a server stores them. They can be removed by editing each claim
|
||||||
by disabling notifications on the Profile <font-awesome icon="circle-user" class="fa-fw" /> page.
|
and deleting the image.
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
If sending images, a server stores them, too. They can be removed by editing the claim
|
|
||||||
and deleting them.
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
If sending other partner system data (eg. to Trustroots) a public key and message
|
If sending other partner system data (eg. to Trustroots) a public key and message
|
||||||
data are stored on a server. Those can be removed via direct personal request.
|
data are stored on a server. Those can be removed via direct personal request (via contact below).
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
For all other claim data,
|
For all other claim data,
|
||||||
|
|||||||
@@ -88,9 +88,15 @@ import { Router } from "vue-router";
|
|||||||
|
|
||||||
import { AppString, NotificationIface } from "../constants/app";
|
import { AppString, NotificationIface } from "../constants/app";
|
||||||
import { DEFAULT_ROOT_DERIVATION_PATH } from "../libs/crypto";
|
import { DEFAULT_ROOT_DERIVATION_PATH } from "../libs/crypto";
|
||||||
import { retrieveAccountCount, importFromMnemonic } from "../libs/util";
|
import {
|
||||||
|
retrieveAccountCount,
|
||||||
|
importFromMnemonic,
|
||||||
|
checkForDuplicateAccount,
|
||||||
|
DUPLICATE_ACCOUNT_ERROR,
|
||||||
|
} from "../libs/util";
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||||
|
import { NOTIFY_DUPLICATE_ACCOUNT_IMPORT } from "@/constants/notifications";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Import Account View Component
|
* Import Account View Component
|
||||||
@@ -198,6 +204,19 @@ export default class ImportAccountView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Check for duplicate account before importing
|
||||||
|
const isDuplicate = await checkForDuplicateAccount(
|
||||||
|
this.mnemonic,
|
||||||
|
this.derivationPath,
|
||||||
|
);
|
||||||
|
if (isDuplicate) {
|
||||||
|
this.notify.warning(
|
||||||
|
NOTIFY_DUPLICATE_ACCOUNT_IMPORT.message,
|
||||||
|
TIMEOUTS.LONG,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await importFromMnemonic(
|
await importFromMnemonic(
|
||||||
this.mnemonic,
|
this.mnemonic,
|
||||||
this.derivationPath,
|
this.derivationPath,
|
||||||
@@ -223,9 +242,20 @@ export default class ImportAccountView extends Vue {
|
|||||||
this.$router.push({ name: "account" });
|
this.$router.push({ name: "account" });
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
this.$logError("Import failed: " + error);
|
this.$logError("Import failed: " + error);
|
||||||
|
|
||||||
|
// Check if this is a duplicate account error from saveNewIdentity
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
if (errorMessage.includes(DUPLICATE_ACCOUNT_ERROR)) {
|
||||||
|
this.notify.warning(
|
||||||
|
NOTIFY_DUPLICATE_ACCOUNT_IMPORT.message,
|
||||||
|
TIMEOUTS.LONG,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.notify.error(
|
this.notify.error(
|
||||||
(error instanceof Error ? error.message : String(error)) ||
|
errorMessage || "Failed to import account.",
|
||||||
"Failed to import account.",
|
|
||||||
TIMEOUTS.LONG,
|
TIMEOUTS.LONG,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ import {
|
|||||||
retrieveAllAccountsMetadata,
|
retrieveAllAccountsMetadata,
|
||||||
retrieveFullyDecryptedAccount,
|
retrieveFullyDecryptedAccount,
|
||||||
saveNewIdentity,
|
saveNewIdentity,
|
||||||
|
checkForDuplicateAccount,
|
||||||
} from "../libs/util";
|
} from "../libs/util";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { Account, AccountEncrypted } from "../db/tables/accounts";
|
import { Account, AccountEncrypted } from "../db/tables/accounts";
|
||||||
@@ -171,6 +172,16 @@ export default class ImportAccountView extends Vue {
|
|||||||
const newId = newIdentifier(address, publicHex, privateHex, newDerivPath);
|
const newId = newIdentifier(address, publicHex, privateHex, newDerivPath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Check for duplicate account before creating
|
||||||
|
const isDuplicate = await checkForDuplicateAccount(newId.did);
|
||||||
|
if (isDuplicate) {
|
||||||
|
this.notify.warning(
|
||||||
|
"This derived account already exists. Please try a different derivation path.",
|
||||||
|
TIMEOUTS.LONG,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await saveNewIdentity(newId, mne, newDerivPath);
|
await saveNewIdentity(newId, mne, newDerivPath);
|
||||||
|
|
||||||
// record that as the active DID
|
// record that as the active DID
|
||||||
|
|||||||
@@ -69,10 +69,17 @@
|
|||||||
<div v-if="claimCountWithHidden > 0" class="border-b border-slate-300 pb-2">
|
<div v-if="claimCountWithHidden > 0" class="border-b border-slate-300 pb-2">
|
||||||
<span>
|
<span>
|
||||||
{{ claimCountWithHiddenText }}
|
{{ claimCountWithHiddenText }}
|
||||||
so if you expected but do not see details from someone then ask them to
|
If you don't see expected info above for someone, ask them to check that
|
||||||
check that their activity is visible to you on their Contacts
|
their activity is visible to you (
|
||||||
<font-awesome icon="users" class="text-slate-500" />
|
<font-awesome icon="arrow-up" class="fa-fw" />
|
||||||
page.
|
<font-awesome icon="eye" class="fa-fw" />
|
||||||
|
) on
|
||||||
|
<a
|
||||||
|
class="text-blue-500 underline cursor-pointer"
|
||||||
|
@click="copyContactsLinkToClipboard"
|
||||||
|
>
|
||||||
|
this page </a
|
||||||
|
>.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="claimCountByUser > 0" class="border-b border-slate-300 pb-2">
|
<div v-if="claimCountByUser > 0" class="border-b border-slate-300 pb-2">
|
||||||
@@ -120,10 +127,11 @@ import { DateTime } from "luxon";
|
|||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
import TopMessage from "../components/TopMessage.vue";
|
import TopMessage from "../components/TopMessage.vue";
|
||||||
import { NotificationIface } from "../constants/app";
|
import { NotificationIface, APP_SERVER } from "../constants/app";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
import {
|
import {
|
||||||
GenericCredWrapper,
|
GenericCredWrapper,
|
||||||
@@ -148,6 +156,7 @@ import {
|
|||||||
NOTIFY_ALL_CONFIRMATIONS_ERROR,
|
NOTIFY_ALL_CONFIRMATIONS_ERROR,
|
||||||
NOTIFY_GIVE_SEND_ERROR,
|
NOTIFY_GIVE_SEND_ERROR,
|
||||||
NOTIFY_CLAIMS_SEND_ERROR,
|
NOTIFY_CLAIMS_SEND_ERROR,
|
||||||
|
NOTIFY_COPIED_TO_CLIPBOARD,
|
||||||
createConfirmationSuccessMessage,
|
createConfirmationSuccessMessage,
|
||||||
createCombinedSuccessMessage,
|
createCombinedSuccessMessage,
|
||||||
} from "@/constants/notifications";
|
} from "@/constants/notifications";
|
||||||
@@ -195,8 +204,8 @@ export default class QuickActionBvcEndView extends Vue {
|
|||||||
get claimCountWithHiddenText() {
|
get claimCountWithHiddenText() {
|
||||||
if (this.claimCountWithHidden === 0) return "";
|
if (this.claimCountWithHidden === 0) return "";
|
||||||
return this.claimCountWithHidden === 1
|
return this.claimCountWithHidden === 1
|
||||||
? "There is 1 other claim with hidden details,"
|
? "There is 1 other claim with hidden details."
|
||||||
: `There are ${this.claimCountWithHidden} other claims with hidden details,`;
|
: `There are ${this.claimCountWithHidden} other claims with hidden details.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get claimCountByUserText() {
|
get claimCountByUserText() {
|
||||||
@@ -296,6 +305,25 @@ export default class QuickActionBvcEndView extends Vue {
|
|||||||
(this.$router as Router).push(route);
|
(this.$router as Router).push(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
copyContactsLinkToClipboard() {
|
||||||
|
const deepLinkUrl = `${APP_SERVER}/deep-link/did/${this.activeDid}`;
|
||||||
|
useClipboard()
|
||||||
|
.copy(deepLinkUrl)
|
||||||
|
.then(() => {
|
||||||
|
this.notify.success(
|
||||||
|
NOTIFY_COPIED_TO_CLIPBOARD.message("Your info link"),
|
||||||
|
TIMEOUTS.SHORT,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error("Failed to copy to clipboard:", error);
|
||||||
|
this.notify.error(
|
||||||
|
"Failed to copy link to clipboard. Please try again.",
|
||||||
|
TIMEOUTS.SHORT,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async record() {
|
async record() {
|
||||||
try {
|
try {
|
||||||
if (this.claimsToConfirmSelected.length > 0) {
|
if (this.claimsToConfirmSelected.length > 0) {
|
||||||
|
|||||||
@@ -69,8 +69,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import { UNNAMED_ENTITY_NAME } from '../src/constants/entities';
|
import { createContactName, generateNewEthrUser, importUser, importUserFromAccount } from './testUtils';
|
||||||
import { deleteContact, generateAndRegisterEthrUser, importUser } from './testUtils';
|
|
||||||
import { NOTIFY_CONTACT_INVALID_DID } from '../src/constants/notifications';
|
import { NOTIFY_CONTACT_INVALID_DID } from '../src/constants/notifications';
|
||||||
|
|
||||||
test('Check activity feed - check that server is running', async ({ page }) => {
|
test('Check activity feed - check that server is running', async ({ page }) => {
|
||||||
@@ -185,35 +184,20 @@ test('Check invalid DID shows error and redirects', async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Check User 0 can register a random person', async ({ page }) => {
|
test('Check User 0 can register a random person', async ({ page }) => {
|
||||||
await importUser(page, '00');
|
const newDid = await generateNewEthrUser(page); // generate a new user
|
||||||
const newDid = await generateAndRegisterEthrUser(page);
|
|
||||||
expect(newDid).toContain('did:ethr:');
|
|
||||||
|
|
||||||
await page.goto('./');
|
await importUserFromAccount(page, "00"); // switch to User Zero
|
||||||
await page.getByTestId('closeOnboardingAndFinish').click();
|
|
||||||
await page.getByRole('button', { name: 'Person' }).click();
|
|
||||||
await page.getByRole('listitem').filter({ hasText: UNNAMED_ENTITY_NAME }).locator('svg').click();
|
|
||||||
await page.getByPlaceholder('What was given').fill('Gave me access!');
|
|
||||||
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
|
||||||
await expect(page.getByText('That gift was recorded.')).toBeVisible();
|
|
||||||
// now ensure that alert goes away
|
|
||||||
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss alert
|
|
||||||
await expect(page.getByText('That gift was recorded.')).toBeHidden();
|
|
||||||
|
|
||||||
// now delete the contact to test that pages still do reasonable things
|
// As User Zero, add the new user as a contact
|
||||||
await deleteContact(page, newDid);
|
await page.goto('./contacts');
|
||||||
// go the activity page for this new person
|
const contactName = createContactName(newDid);
|
||||||
await page.goto('./did/' + encodeURIComponent(newDid));
|
await page.getByPlaceholder('URL or DID, Name, Public Key').fill(`${newDid}, ${contactName}`);
|
||||||
// maybe replace by: const popupPromise = page.waitForEvent('popup');
|
await expect(page.locator('button > svg.fa-plus')).toBeVisible();
|
||||||
let error;
|
await page.locator('button > svg.fa-plus').click();
|
||||||
try {
|
await expect(page.locator('div[role="alert"] h4:has-text("Success")')).toBeVisible(); // wait for info alert to be visible…
|
||||||
await page.waitForSelector('div[role="alert"]', { timeout: 2000 });
|
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // …and dismiss it
|
||||||
error = new Error('Error alert should not show.');
|
await expect(page.locator('div[role="alert"] button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone
|
||||||
} catch (error) {
|
await page.locator('div[role="alert"] button:text-is("Yes")').click(); // Register new contact
|
||||||
// success
|
await page.locator('div[role="alert"] button:text-is("No, Not Now")').click(); // Dismiss export data prompt
|
||||||
} finally {
|
await expect(page.locator("li", { hasText: contactName })).toBeVisible();
|
||||||
if (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
63
test-playwright/03-duplicate-import-test.spec.ts
Normal file
63
test-playwright/03-duplicate-import-test.spec.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { importUserFromAccount, getTestUserData } from './testUtils';
|
||||||
|
import { NOTIFY_DUPLICATE_ACCOUNT_IMPORT } from '../src/constants/notifications';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test duplicate account import functionality
|
||||||
|
*
|
||||||
|
* This test verifies that:
|
||||||
|
* 1. A user can successfully import an account the first time
|
||||||
|
* 2. Attempting to import the same account again shows a warning message
|
||||||
|
* 3. The duplicate import is prevented
|
||||||
|
*/
|
||||||
|
test.describe('Duplicate Account Import', () => {
|
||||||
|
test('should prevent importing the same account twice', async ({ page }) => {
|
||||||
|
const userData = getTestUserData("00");
|
||||||
|
|
||||||
|
// First import - should succeed
|
||||||
|
await page.goto("./start");
|
||||||
|
await page.getByText("You have a seed").click();
|
||||||
|
await page.getByPlaceholder("Seed Phrase").fill(userData.seedPhrase);
|
||||||
|
await page.getByRole("button", { name: "Import" }).click();
|
||||||
|
|
||||||
|
// Verify first import was successful
|
||||||
|
await expect(page.getByRole("code")).toContainText(userData.did);
|
||||||
|
|
||||||
|
// Navigate back to start page for second import attempt
|
||||||
|
await page.goto("./start");
|
||||||
|
await page.getByText("You have a seed").click();
|
||||||
|
await page.getByPlaceholder("Seed Phrase").fill(userData.seedPhrase);
|
||||||
|
await page.getByRole("button", { name: "Import" }).click();
|
||||||
|
|
||||||
|
// Verify duplicate import shows warning message
|
||||||
|
// The warning can appear either from the pre-check or from the saveNewIdentity error handling
|
||||||
|
await expect(page.getByText(NOTIFY_DUPLICATE_ACCOUNT_IMPORT.message)).toBeVisible();
|
||||||
|
|
||||||
|
// Verify we're still on the import page (not redirected to account)
|
||||||
|
await expect(page.getByPlaceholder("Seed Phrase")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow importing different accounts', async ({ page }) => {
|
||||||
|
const userZeroData = getTestUserData("00");
|
||||||
|
const userOneData = getTestUserData("01");
|
||||||
|
|
||||||
|
// Import first user
|
||||||
|
await page.goto("./start");
|
||||||
|
await page.getByText("You have a seed").click();
|
||||||
|
await page.getByPlaceholder("Seed Phrase").fill(userZeroData.seedPhrase);
|
||||||
|
await page.getByRole("button", { name: "Import" }).click();
|
||||||
|
|
||||||
|
// Verify first import was successful
|
||||||
|
await expect(page.getByRole("code")).toContainText(userZeroData.did);
|
||||||
|
|
||||||
|
// Navigate back to start page for second user import
|
||||||
|
await page.goto("./start");
|
||||||
|
await page.getByText("You have a seed").click();
|
||||||
|
await page.getByPlaceholder("Seed Phrase").fill(userOneData.seedPhrase);
|
||||||
|
await page.getByRole("button", { name: "Import" }).click();
|
||||||
|
|
||||||
|
// Verify second import was successful (should not show duplicate warning)
|
||||||
|
await expect(page.getByRole("code")).toContainText(userOneData.did);
|
||||||
|
await expect(page.getByText(NOTIFY_DUPLICATE_ACCOUNT_IMPORT.message)).not.toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
* - Custom expiration date
|
* - Custom expiration date
|
||||||
* 2. The invitation appears in the list after creation
|
* 2. The invitation appears in the list after creation
|
||||||
* 3. A new user can accept the invitation and become connected
|
* 3. A new user can accept the invitation and become connected
|
||||||
|
* 4. The new user can create gift records from the front page
|
||||||
*
|
*
|
||||||
* Test Flow:
|
* Test Flow:
|
||||||
* 1. Imports User 0 (test account)
|
* 1. Imports User 0 (test account)
|
||||||
@@ -19,6 +20,8 @@
|
|||||||
* 4. Creates a new user with Ethr DID
|
* 4. Creates a new user with Ethr DID
|
||||||
* 5. Accepts the invitation as the new user
|
* 5. Accepts the invitation as the new user
|
||||||
* 6. Verifies the connection is established
|
* 6. Verifies the connection is established
|
||||||
|
* 7. Tests that the new user can create gift records from the front page
|
||||||
|
* 8. Verifies the gift appears in the home view
|
||||||
*
|
*
|
||||||
* Related Files:
|
* Related Files:
|
||||||
* - Frontend invite handling: src/libs/endorserServer.ts
|
* - Frontend invite handling: src/libs/endorserServer.ts
|
||||||
@@ -29,7 +32,7 @@
|
|||||||
* @requires ./testUtils - For user management utilities
|
* @requires ./testUtils - For user management utilities
|
||||||
*/
|
*/
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import { deleteContact, generateNewEthrUser, generateRandomString, importUser, switchToUser } from './testUtils';
|
import { createGiftFromFrontPageForNewUser, deleteContact, generateNewEthrUser, generateRandomString, importUser, switchToUser } from './testUtils';
|
||||||
|
|
||||||
test('Check User 0 can invite someone', async ({ page }) => {
|
test('Check User 0 can invite someone', async ({ page }) => {
|
||||||
await importUser(page, '00');
|
await importUser(page, '00');
|
||||||
@@ -58,4 +61,7 @@ test('Check User 0 can invite someone', async ({ page }) => {
|
|||||||
await page.locator('button:has-text("Save")').click();
|
await page.locator('button:has-text("Save")').click();
|
||||||
await expect(page.locator('button:has-text("Save")')).toBeHidden();
|
await expect(page.locator('button:has-text("Save")')).toBeHidden();
|
||||||
await expect(page.locator(`li:has-text("My pal User #0")`)).toBeVisible();
|
await expect(page.locator(`li:has-text("My pal User #0")`)).toBeVisible();
|
||||||
|
|
||||||
|
// Verify the new user can create a gift record from the front page
|
||||||
|
const giftTitle = await createGiftFromFrontPageForNewUser(page, `Gift from new user ${neighborNum}`);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { expect, Page } from "@playwright/test";
|
import { expect, Page } from "@playwright/test";
|
||||||
|
import { UNNAMED_ENTITY_NAME } from '../src/constants/entities';
|
||||||
|
|
||||||
// Get test user data based on the ID.
|
// Get test user data based on the ID.
|
||||||
// '01' -> user 111
|
// '01' -> user 111
|
||||||
@@ -109,7 +110,7 @@ export async function switchToUser(page: Page, did: string): Promise<void> {
|
|||||||
await page.getByTestId("didWrapper").locator('code:has-text("did:")');
|
await page.getByTestId("didWrapper").locator('code:has-text("did:")');
|
||||||
}
|
}
|
||||||
|
|
||||||
function createContactName(did: string): string {
|
export function createContactName(did: string): string {
|
||||||
return "User " + did.slice(11, 14);
|
return "User " + did.slice(11, 14);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,30 +145,6 @@ export async function generateNewEthrUser(page: Page): Promise<string> {
|
|||||||
return newDid;
|
return newDid;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a new random user and register them.
|
|
||||||
// Note that this makes 000 the active user. Use switchToUser to switch to this DID.
|
|
||||||
export async function generateAndRegisterEthrUser(page: Page): Promise<string> {
|
|
||||||
const newDid = await generateNewEthrUser(page);
|
|
||||||
|
|
||||||
await importUser(page, "000"); // switch to user 000
|
|
||||||
|
|
||||||
await page.goto("./contacts");
|
|
||||||
const contactName = createContactName(newDid);
|
|
||||||
await page
|
|
||||||
.getByPlaceholder("URL or DID, Name, Public Key")
|
|
||||||
.fill(`${newDid}, ${contactName}`);
|
|
||||||
await page.locator("button > svg.fa-plus").click();
|
|
||||||
// register them
|
|
||||||
await page.locator('div[role="alert"] button:text-is("Yes")').click();
|
|
||||||
// wait for it to disappear because the next steps may depend on alerts being gone
|
|
||||||
await expect(
|
|
||||||
page.locator('div[role="alert"] button:text-is("Yes")')
|
|
||||||
).toBeHidden();
|
|
||||||
await expect(page.locator("li", { hasText: contactName })).toBeVisible();
|
|
||||||
|
|
||||||
return newDid;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to generate a random string of specified length
|
// Function to generate a random string of specified length
|
||||||
export async function generateRandomString(length: number): Promise<string> {
|
export async function generateRandomString(length: number): Promise<string> {
|
||||||
return Math.random()
|
return Math.random()
|
||||||
@@ -239,3 +216,44 @@ export function isResourceIntensiveTest(testPath: string): boolean {
|
|||||||
testPath.includes("40-add-contact")
|
testPath.includes("40-add-contact")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a gift record from the front page
|
||||||
|
* @param page - Playwright page object
|
||||||
|
* @param giftTitle - Optional custom title, defaults to "Gift " + random string
|
||||||
|
* @param amount - Optional amount, defaults to random 1-99
|
||||||
|
* @returns Promise resolving to the created gift title
|
||||||
|
*/
|
||||||
|
export async function createGiftFromFrontPageForNewUser(
|
||||||
|
page: Page,
|
||||||
|
giftTitle?: string,
|
||||||
|
amount?: number
|
||||||
|
): Promise<void> {
|
||||||
|
// Generate random values if not provided
|
||||||
|
const randomString = Math.random().toString(36).substring(2, 6);
|
||||||
|
const finalTitle = giftTitle || `Gift ${randomString}`;
|
||||||
|
const finalAmount = amount || Math.floor(Math.random() * 99) + 1;
|
||||||
|
|
||||||
|
// Navigate to home page and close onboarding
|
||||||
|
await page.goto('./');
|
||||||
|
await page.getByTestId('closeOnboardingAndFinish').click();
|
||||||
|
|
||||||
|
// Start gift creation flow
|
||||||
|
await page.getByRole('button', { name: 'Person' }).click();
|
||||||
|
await page.getByRole('listitem').filter({ hasText: UNNAMED_ENTITY_NAME }).locator('svg').click();
|
||||||
|
|
||||||
|
// Fill gift details
|
||||||
|
await page.getByPlaceholder('What was given').fill(finalTitle);
|
||||||
|
await page.getByRole('spinbutton').fill(finalAmount.toString());
|
||||||
|
|
||||||
|
// Submit gift
|
||||||
|
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
||||||
|
|
||||||
|
// Verify success
|
||||||
|
await expect(page.getByText('That gift was recorded.')).toBeVisible();
|
||||||
|
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert
|
||||||
|
|
||||||
|
// Verify the gift appears in the home view
|
||||||
|
await page.goto('./');
|
||||||
|
await expect(page.locator('ul#listLatestActivity li').filter({ hasText: giftTitle })).toBeVisible();
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user