Browse Source

Merge branch 'master' into seed-phrase-backup-prompt

pull/195/head
Jose Olarte 3 1 week ago
parent
commit
f34c567ab4
  1. 22
      src/libs/endorserServer.ts
  2. 35
      src/libs/util.ts
  3. 64
      src/services/QRScanner/WebInlineQRScanner.ts
  4. 86
      src/views/ClaimView.vue
  5. 71
      src/views/ConfirmGiftView.vue
  6. 8
      test-playwright/05-invite.spec.ts
  7. 42
      test-playwright/testUtils.ts

22
src/libs/endorserServer.ts

@ -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

35
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 (

64
src/services/QRScanner/WebInlineQRScanner.ts

@ -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) {

86
src/views/ClaimView.vue

@ -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,9 +108,9 @@
</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/' +
@ -116,30 +118,35 @@
" "
class="text-blue-500 mt-2" class="text-blue-500 mt-2"
> >
Fulfills a bigger plan... This fulfills a bigger plan
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</router-link> </router-link>
</div> </div>
<!-- if there's another, it's probably fulfilling an offer, too -->
<div <!-- Show offer fulfillment if this give fulfills an offer -->
v-if=" <div v-if="detailsForGiveOfferFulfillment?.offerHandleId">
detailsForGive?.fulfillsType &&
detailsForGive?.fulfillsType !== 'PlanAction' &&
detailsForGive?.fulfillsHandleId
"
>
<!-- router-link to /claim/ only changes URL path --> <!-- router-link to /claim/ only changes URL path -->
<a <a
class="text-blue-500 mt-4 cursor-pointer" class="text-blue-500 mt-4 cursor-pointer"
@click=" @click="
showDifferentClaimPage(detailsForGive?.fulfillsHandleId) showDifferentClaimPage(
detailsForGiveOfferFulfillment.offerHandleId,
)
" "
> >
Fulfills This fulfills
{{ {{
capitalizeAndInsertSpacesBeforeCaps( serverUtil.capitalizeAndInsertSpacesBeforeCapsWithAPrefix(
detailsForGive.fulfillsType, detailsForGiveOfferFulfillment.offerType || "Offer",
) )
}}... }}
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</a> </a>
</div> </div>
@ -152,12 +159,16 @@
" "
class="text-blue-500 mt-4" class="text-blue-500 mt-4"
> >
Offered to a bigger plan... Offered to a bigger plan
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</router-link> </router-link>
</div> </div>
<!-- Providers --> <!-- Providers -->
<div v-if="providersForGive?.length > 0" class="mt-4"> <div v-if="providersForGive?.length > 0">
<span>Other assistance provided by:</span> <span>Other assistance provided by:</span>
<ul class="ml-4"> <ul class="ml-4">
<li <li
@ -171,7 +182,11 @@
class="text-blue-500 mt-4 cursor-pointer" class="text-blue-500 mt-4 cursor-pointer"
@click="handleProviderClick(provider)" @click="handleProviderClick(provider)"
> >
an activity... an activity
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</a> </a>
</div> </div>
</div> </div>
@ -182,6 +197,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="mt-2"> <div class="mt-2">
<font-awesome icon="comment" class="text-slate-400" /> <font-awesome icon="comment" class="text-slate-400" />
{{ issuerName }} posted that. {{ issuerName }} posted that.
@ -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),

71
src/views/ConfirmGiftView.vue

@ -96,13 +96,15 @@
</div> </div>
<!-- Fullfills Links --> <!-- Fullfills Links -->
<div class="mt-4">
<!-- fullfills links for a give --> <!-- fullfills links for a give -->
<div v-if="giveDetails?.fulfillsPlanHandleId" class="mt-2"> <div v-if="giveDetails?.fulfillsPlanHandleId">
<router-link <router-link
:to=" :to="
'/project/' + '/project/' +
encodeURIComponent(giveDetails?.fulfillsPlanHandleId || '') encodeURIComponent(
giveDetails?.fulfillsPlanHandleId || '',
)
" "
class="text-blue-500 mt-2 cursor-pointer" class="text-blue-500 mt-2 cursor-pointer"
> >
@ -113,26 +115,23 @@
/> />
</router-link> </router-link>
</div> </div>
<!-- if there's another, it's probably fulfilling an offer, too -->
<div <!-- Show offer fulfillment if this give fulfills an offer -->
v-if=" <div v-if="giveDetailsOfferFulfillment?.offerHandleId">
giveDetails?.fulfillsType &&
giveDetails?.fulfillsType !== 'PlanAction' &&
giveDetails?.fulfillsHandleId
"
>
<!-- router-link to /claim/ only changes URL path --> <!-- router-link to /claim/ only changes URL path -->
<router-link <router-link
:to=" :to="
'/claim/' + '/claim/' +
encodeURIComponent(giveDetails?.fulfillsHandleId || '') encodeURIComponent(
giveDetailsOfferFulfillment.offerHandleId || '',
)
" "
class="text-blue-500 mt-2 cursor-pointer" class="text-blue-500 mt-2 cursor-pointer"
> >
This fulfills This fulfills
{{ {{
capitalizeAndInsertSpacesBeforeCapsWithAPrefix( serverUtil.capitalizeAndInsertSpacesBeforeCapsWithAPrefix(
giveDetails?.fulfillsType || "", giveDetailsOfferFulfillment.offerType || "Offer",
) )
}} }}
<font-awesome <font-awesome
@ -145,6 +144,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="mt-2"> <div class="mt-2">
<font-awesome icon="comment" class="text-slate-400" /> <font-awesome icon="comment" class="text-slate-400" />
{{ issuerName }} posted that. {{ issuerName }} posted that.
@ -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>

8
test-playwright/05-invite.spec.ts

@ -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}`);
}); });

42
test-playwright/testUtils.ts

@ -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
@ -215,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();
}

Loading…
Cancel
Save