+
+
diff --git a/src/views/DiscoverView.vue b/src/views/DiscoverView.vue
index 4f73a836f..61b0669c2 100644
--- a/src/views/DiscoverView.vue
+++ b/src/views/DiscoverView.vue
@@ -265,6 +265,8 @@ export default class DiscoverView extends Vue {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
console.error("Error with feed load:", e);
+ // this sometimes gives different information
+ console.error("Error with feed load (error added): " + e);
this.$notify(
{
group: "alert",
diff --git a/src/views/GiftedDetails.vue b/src/views/GiftedDetailsView.vue
similarity index 98%
rename from src/views/GiftedDetails.vue
rename to src/views/GiftedDetailsView.vue
index 3e1466c5a..dfdb18969 100644
--- a/src/views/GiftedDetails.vue
+++ b/src/views/GiftedDetailsView.vue
@@ -269,6 +269,7 @@ export default class GiftedDetails extends Vue {
this.hideBackButton =
(this.$route as Router).query["hideBackButton"] === "true";
this.message = ((this.$route as Router).query["message"] as string) || "";
+
// find any offer ID
const fulfills = this.prevCredToEdit?.claim?.fulfills;
const fulfillsArray = Array.isArray(fulfills)
@@ -351,6 +352,7 @@ export default class GiftedDetails extends Vue {
);
}
}
+ // these should be functions but something's wrong with the syntax in the <> conditional
this.givenToProject = !!this.projectId;
this.givenToRecipient = !this.givenToProject && !!this.recipientDid;
@@ -549,7 +551,7 @@ export default class GiftedDetails extends Vue {
group: "alert",
type: "warning",
title: "Error",
- text: "To assign to a project, you must open this dialog through a project.",
+ text: "To assign to a project, you must open this page through a project.",
},
3000,
);
@@ -574,7 +576,7 @@ export default class GiftedDetails extends Vue {
group: "alert",
type: "warning",
title: "Error",
- text: "To assign to a recipient, you must open this dialog from a contact.",
+ text: "To assign to a recipient, you must open this page from a contact.",
},
3000,
);
@@ -694,7 +696,6 @@ export default class GiftedDetails extends Vue {
constructGiveParam() {
const recipientDid = this.givenToRecipient ? this.recipientDid : undefined;
const projectId = this.givenToProject ? this.projectId : undefined;
- // const giveClaim = constructGive(
const giveClaim = hydrateGive(
this.prevCredToEdit?.claim as GiveVerifiableCredential,
this.giverDid,
diff --git a/src/views/NewEditProjectView.vue b/src/views/NewEditProjectView.vue
index 6d5f4d0f8..c48613573 100644
--- a/src/views/NewEditProjectView.vue
+++ b/src/views/NewEditProjectView.vue
@@ -97,8 +97,8 @@
/>
@@ -309,7 +309,7 @@ export default class NewEditProjectView extends Vue {
return;
}
try {
- const headers = getHeaders(this.activeDid) as AxiosRequestHeaders;
+ const headers = (await getHeaders(this.activeDid)) as AxiosRequestHeaders;
const response = await this.axios.delete(
DEFAULT_IMAGE_API_SERVER +
"/image/" +
diff --git a/src/views/OfferDetailsView.vue b/src/views/OfferDetailsView.vue
new file mode 100644
index 000000000..d50f92860
--- /dev/null
+++ b/src/views/OfferDetailsView.vue
@@ -0,0 +1,633 @@
+
+
+
diff --git a/src/views/ProjectViewView.vue b/src/views/ProjectViewView.vue
index 242960a25..72be8d206 100644
--- a/src/views/ProjectViewView.vue
+++ b/src/views/ProjectViewView.vue
@@ -162,6 +162,7 @@
diff --git a/src/views/ProjectsView.vue b/src/views/ProjectsView.vue
index 663b331f1..d9394775c 100644
--- a/src/views/ProjectsView.vue
+++ b/src/views/ProjectsView.vue
@@ -109,6 +109,19 @@
+
+ To
+ {{
+ offer.fulfillsPlanHandleId
+ ? projectNameFromHandleId[offer.fulfillsPlanHandleId]
+ : didInfo(
+ offer.recipientDid,
+ activeDid,
+ allMyDids,
+ allContacts,
+ )
+ }}
+
{{ offer.objectDescription }}
@@ -244,11 +257,14 @@ import QuickNav from "@/components/QuickNav.vue";
import ProjectIcon from "@/components/ProjectIcon.vue";
import TopMessage from "@/components/TopMessage.vue";
import {
+ didInfo,
getHeaders,
+ getPlanFromCache,
OfferSummaryRecord,
PlanData,
} from "@/libs/endorserServer";
import EntityIcon from "@/components/EntityIcon.vue";
+import { Contact } from "@/db/tables/contacts";
@Component({
components: { EntityIcon, InfiniteScroll, QuickNav, ProjectIcon, TopMessage },
@@ -263,16 +279,19 @@ export default class ProjectsView extends Vue {
}
activeDid = "";
+ allContacts: Array
= [];
+ allMyDids: Array = [];
apiServer = "";
projects: PlanData[] = [];
isLoading = false;
isRegistered = false;
- numAccounts = 0;
offers: OfferSummaryRecord[] = [];
+ projectNameFromHandleId: Record = {}; // mapping from handleId to description
showOffers = true;
showProjects = false;
libsUtil = libsUtil;
+ didInfo = didInfo;
async mounted() {
try {
@@ -282,9 +301,13 @@ export default class ProjectsView extends Vue {
this.apiServer = (settings?.apiServer as string) || "";
this.isRegistered = !!settings?.isRegistered;
+ this.allContacts = await db.contacts.toArray();
+
await accountsDB.open();
- this.numAccounts = await accountsDB.accounts.count();
- if (this.numAccounts === 0) {
+ const allAccounts = await accountsDB.accounts.toArray();
+ this.allMyDids = allAccounts.map((acc) => acc.did);
+
+ if (allAccounts.length === 0) {
console.error("No accounts found.");
this.errNote("You need an identifier to load your projects.");
} else {
@@ -343,10 +366,7 @@ export default class ProjectsView extends Vue {
async loadMoreProjectData(payload: boolean) {
if (this.projects.length > 0 && payload) {
const latestProject = this.projects[this.projects.length - 1];
- await this.loadProjects(
- this.activeDid,
- `beforeId=${latestProject.rowid}`,
- );
+ await this.loadProjects(`beforeId=${latestProject.rowid}`);
}
}
@@ -355,7 +375,7 @@ export default class ProjectsView extends Vue {
* @param issuerDid of the user
* @param urlExtra additional url parameters in a string
**/
- async loadProjects(activeDid?: string, urlExtra: string = "") {
+ async loadProjects(urlExtra: string = "") {
const url = `${this.apiServer}/api/v2/report/plansByIssuer?${urlExtra}`;
await this.projectDataLoader(url);
}
@@ -396,13 +416,37 @@ export default class ProjectsView extends Vue {
* @param token Authorization token
**/
async offerDataLoader(url: string) {
- const headers = getHeaders(this.activeDid);
+ const headers = await getHeaders(this.activeDid);
try {
this.isLoading = true;
const resp = await this.axios.get(url, { headers } as AxiosRequestConfig);
if (resp.status === 200 && resp.data.data) {
- this.offers = this.offers.concat(resp.data.data);
+ // add one-by-one as they retrieve project names, potentially from the server
+ for (const offer of resp.data.data) {
+ if (offer.fulfillsPlanHandleId) {
+ const project = await getPlanFromCache(
+ offer.fulfillsPlanHandleId,
+ this.axios,
+ this.apiServer,
+ this.activeDid,
+ );
+ const projectName = project?.name as string;
+ console.log(
+ "now have name for",
+ offer.fulfillsPlanHandleId,
+ projectName,
+ );
+ this.projectNameFromHandleId[offer.fulfillsPlanHandleId] =
+ projectName;
+ console.log(
+ "now have a real name for",
+ offer.fulfillsPlanHandleId,
+ this.projectNameFromHandleId[offer.fulfillsPlanHandleId],
+ );
+ }
+ this.offers = this.offers.concat([offer]);
+ }
} else {
console.error(
"Bad server response & data for offers:",
@@ -443,7 +487,7 @@ export default class ProjectsView extends Vue {
async loadMoreOfferData(payload: boolean) {
if (this.offers.length > 0 && payload) {
const latestOffer = this.offers[this.offers.length - 1];
- await this.loadOffers(this.activeDid, `&beforeId=${latestOffer.jwtId}`);
+ await this.loadOffers(`&beforeId=${latestOffer.jwtId}`);
}
}
@@ -452,8 +496,8 @@ export default class ProjectsView extends Vue {
* @param issuerDid of the user
* @param urlExtra additional url parameters in a string
**/
- async loadOffers(issuerDid?: string, urlExtra: string = "") {
- const url = `${this.apiServer}/api/v2/report/offers?offeredByDid=${issuerDid}${urlExtra}`;
+ async loadOffers(urlExtra: string = "") {
+ const url = `${this.apiServer}/api/v2/report/offers?offeredByDid=${this.activeDid}${urlExtra}`;
await this.offerDataLoader(url);
}
diff --git a/src/views/SharedPhotoView.vue b/src/views/SharedPhotoView.vue
index 2eb385462..1bd583e43 100644
--- a/src/views/SharedPhotoView.vue
+++ b/src/views/SharedPhotoView.vue
@@ -48,6 +48,17 @@
No image found.
+
+ If you shared an image, the cause is usually that you do not have the
+ recent version of this app, or that the app has not refreshed the
+ service code underneath. To fix this, first make sure you have latest
+ version by comparing your version at the bottom of "Help" with the
+ version at the bottom of https://timesafari.app/help in a browser. After
+ that, it may eventually work, but you can speed up the process by
+ clearing your data cache (in the browser on mobile, even if you
+ installed it) and/or reinstalling the app (after backing up all your
+ data, of course).
+
@@ -122,7 +133,7 @@ export default class SharedPhotoView extends Vue {
name: "gifted-details",
// this might be wrong since "name" goes with params, but it works so test well when you change it
query: {
- destinationPathAfter: "/home",
+ destinationPathAfter: "/",
hideBackButton: true,
imageUrl: url,
recipientDid: this.activeDid,
diff --git a/sw_scripts/safari-notifications.js b/sw_scripts/safari-notifications.js
index 0103f5beb..5ba7e06a9 100644
--- a/sw_scripts/safari-notifications.js
+++ b/sw_scripts/safari-notifications.js
@@ -566,14 +566,27 @@ async function getNotificationCount() {
return result;
}
+async function blobToBase64String(blob) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onloadend = () => resolve(reader.result); // potential problem if it returns an ArrayBuffer?
+ reader.onerror = reject;
+ reader.readAsDataURL(blob);
+ });
+}
+
// Store the image blob and go immediate to a page to upload it.
// @param photo - image Blob to store for later retrieval after redirect
async function savePhoto(photo) {
try {
+ const photoBase64 = await blobToBase64String(photo);
const db = await openIndexedDB("TimeSafari");
const transaction = db.transaction("temp", "readwrite");
const store = transaction.objectStore("temp");
- await updateRecord(store, { id: "shared-photo", blob: photo });
+ await updateRecord(store, {
+ id: "shared-photo-base64",
+ blobB64: photoBase64,
+ });
transaction.oncomplete = () => db.close();
} catch (error) {
console.error("safari-notifications logMessage IndexedDB error", error);
diff --git a/test-playwright/20-create-project.spec.ts b/test-playwright/20-create-project.spec.ts
index af22ff0b0..4564a7864 100644
--- a/test-playwright/20-create-project.spec.ts
+++ b/test-playwright/20-create-project.spec.ts
@@ -12,8 +12,8 @@ test('Create new project, then search for it', async ({ page }) => {
const finalRandomString = randomString.substring(0, 16);
// Standard texts
- const standardTitle = "Idea ";
- const standardDescription = "Description of Idea ";
+ const standardTitle = 'Idea ';
+ const standardDescription = 'Description of Idea ';
// Combine texts with the random string
const finalTitle = standardTitle + finalRandomString;
@@ -43,12 +43,10 @@ test('Create new project, then search for it', async ({ page }) => {
// Search for newly-created project in /projects
await page.goto('./projects');
await page.getByRole('link', { name: 'Projects', exact: true }).click();
- await page.waitForTimeout(3000); // Wait for a bit
await expect(page.locator('ul#listProjects li.border-b:nth-child(1)')).toContainText(finalRandomString); // Assumes newest project always appears first in the Projects tab list
// Search for newly-created project in /discover
await page.goto('./discover');
- await page.waitForTimeout(3000); // Wait for a bit
await page.getByPlaceholder('Search…').fill(finalRandomString);
await page.locator('#QuickSearch button').click();
await expect(page.locator('ul#listDiscoverResults li.border-b:nth-child(1)')).toContainText(finalRandomString);
diff --git a/test-playwright/30-record-gift.spec.ts b/test-playwright/30-record-gift.spec.ts
index 387be37fc..391ce7151 100644
--- a/test-playwright/30-record-gift.spec.ts
+++ b/test-playwright/30-record-gift.spec.ts
@@ -2,23 +2,17 @@ import { test, expect } from '@playwright/test';
import { importUser } from './testUtils';
test('Record something given', async ({ page }) => {
- // Generate a random string of 16 characters
- let randomString = Math.random().toString(36).substring(2, 18);
-
- // In case the string is shorter than 16 characters, generate more characters until it is 16 characters long
- while (randomString.length < 16) {
- randomString += Math.random().toString(36).substring(2, 18);
- }
- const finalRandomString = randomString.substring(0, 16);
+ // Generate a random string of a few characters
+ const randomString = Math.random().toString(36).substring(2, 6);
// Generate a random non-zero single-digit number
const randomNonZeroNumber = Math.floor(Math.random() * 99) + 1;
// Standard title prefix
- const standardTitle = "Gift ";
+ const standardTitle = 'Gift ';
// Combine title prefix with the random string
- const finalTitle = standardTitle + finalRandomString;
+ const finalTitle = standardTitle + randomString;
// Import user 00
await importUser(page, '00');
@@ -27,7 +21,7 @@ test('Record something given', async ({ page }) => {
await page.goto('./');
await page.getByRole('heading', { name: 'Unnamed/Unknown' }).click();
await page.getByPlaceholder('What was given').fill(finalTitle);
- await page.getByRole('spinbutton', { id: 'inputGivenAmount' }).fill(randomNonZeroNumber.toString());
+ await page.getByRole('spinbutton').fill(randomNonZeroNumber.toString());
await page.getByRole('button', { name: 'Sign & Send' }).click();
await expect(page.getByText('That gift was recorded.')).toBeVisible();
diff --git a/test-playwright/35-record-gift-from-image-share.spec.ts b/test-playwright/35-record-gift-from-image-share.spec.ts
index 5e9d9ec0c..72223e501 100644
--- a/test-playwright/35-record-gift-from-image-share.spec.ts
+++ b/test-playwright/35-record-gift-from-image-share.spec.ts
@@ -1,5 +1,6 @@
import path from 'path';
import { test, expect } from '@playwright/test';
+import { importUser } from './testUtils';
test('Record item given from image-share', async ({ page }) => {
@@ -8,11 +9,7 @@ test('Record item given from image-share', async ({ page }) => {
// Combine title prefix with the random string
const finalTitle = `Gift ${randomString} from image-share`;
- // Create new ID using seed phrase "rigid shrug mobile…"
- await page.goto('./start');
- await page.getByText('You have a seed').click();
- await page.getByPlaceholder('Seed Phrase').fill('rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage');
- await page.getByRole('button', { name: 'Import' }).click();
+ await importUser(page, '00');
// Record something given
await page.goto('./test');
@@ -37,4 +34,36 @@ test('Record item given from image-share', async ({ page }) => {
await page.goto('./');
const item1 = page.locator('li').filter({ hasText: finalTitle });
await expect(item1.getByRole('img')).toBeVisible();
-});
\ No newline at end of file
+});
+
+// // I believe there's a way to test this service worker feature.
+// // The following is what I got from ChatGPT. I wonder if it doesn't work because it's not registering the service worker correctly.
+//
+// test('Trigger a photo-sharing fetch event in service worker with POST to /share-target', async ({ page }) => {
+// await importUser(page, '00');
+//
+// // Create a FormData object with a photo
+// const photoPath = path.join(__dirname, '..', 'public', 'img', 'icons', 'android-chrome-192x192.png');
+// const photoContent = await fs.readFileSync(photoPath);
+// const [response] = await Promise.all([
+// page.waitForResponse(response => response.url().includes('/share-target')), // also check for response.status() === 303 ?
+// page.evaluate(async (photoContent) => {
+// const formData = new FormData();
+// formData.append('photo', new Blob([photoContent], { type: 'image/png' }), 'test-photo.jpg');
+//
+// const response = await fetch('/share-target', {
+// method: 'POST',
+// body: formData,
+// });
+//
+// return response;
+// }, photoContent)
+// ]);
+//
+// // Verify the response redirected to /shared-photo
+// //expect(response.status).toBe(303);
+// console.log('response headers', response.headers());
+// console.log('response status', response.status());
+// console.log('response url', response.url());
+// expect(response.url()).toContain('/shared-photo');
+// });
diff --git a/test-playwright/40-add-contact.spec.ts b/test-playwright/40-add-contact.spec.ts
index f6254745f..c44c9ba71 100644
--- a/test-playwright/40-add-contact.spec.ts
+++ b/test-playwright/40-add-contact.spec.ts
@@ -15,20 +15,20 @@ test('Add contact, record gift, confirm gift', async ({ page }) => {
const randomNonZeroNumber = Math.floor(Math.random() * 99) + 1;
// Standard title prefix
- const standardTitle = "Gift ";
+ const standardTitle = 'Gift ';
// Combine title prefix with the random string
const finalTitle = standardTitle + finalRandomString;
// Contact name
- const contactName = 'Contact 00';
+ const contactName = 'Contact #000';
// Import user 01
await importUser(page, '01');
// Add new contact 00
await page.goto('./contacts');
- await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F');
+ await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F, User #000');
await page.locator('button > svg.fa-plus').click();
await expect(page.locator('div[role="alert"]')).toBeVisible();
@@ -36,10 +36,11 @@ test('Add contact, record gift, confirm gift', async ({ page }) => {
// await page.locator('div[role="alert"] button:has-text("Yes")').click();
// Verify added contact
- await expect(page.locator('li.border-b')).toContainText('did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F');
+ await expect(page.locator('li.border-b')).toContainText('User #000');
// Rename contact
- await page.locator('li.border-b h2 > button[title="Edit"]').click();
+ await page.locator('li.border-b div div > a[title="See more about this person"]').click();
+ await page.locator('h2 > button[title="Edit"]').click();
await expect(page.locator('div.dialog-overlay > div.dialog').filter({ hasText: 'Edit Name' })).toBeVisible();
await page.getByPlaceholder('Name', { exact: true }).fill(contactName);
await page.locator('.dialog > .flex > button').first().click();
@@ -82,4 +83,4 @@ test('Add contact, record gift, confirm gift', async ({ page }) => {
// Refresh claim page, Confirm button should be hidden
await page.reload();
await expect(page.getByRole('button', { name: 'Confirm' })).toBeHidden();
-});
\ No newline at end of file
+});
diff --git a/test-playwright/50-record-offer.spec.ts b/test-playwright/50-record-offer.spec.ts
new file mode 100644
index 000000000..f35ab113c
--- /dev/null
+++ b/test-playwright/50-record-offer.spec.ts
@@ -0,0 +1,63 @@
+import { test, expect } from '@playwright/test';
+import { importUser } from './testUtils';
+
+test('Record an offer', async ({ page }) => {
+ // Generate a random string of 3 characters, skipping the "0." at the beginning
+ const randomString = Math.random().toString(36).substring(2, 5);
+ // Standard title prefix
+ const description = `Offering of ${randomString}`;
+ const updatedDescription = `Updated ${description}`;
+ const randomNonZeroNumber = Math.floor(Math.random() * 998) + 1;
+
+ // Create new ID for default user
+ await importUser(page);
+
+ // Select a project
+ await page.goto('./discover');
+ await page.locator('ul#listDiscoverResults li:nth-child(1)').click();
+
+ // Record an offer
+ await page.getByTestId('offerButton').click();
+ await page.getByTestId('inputDescription').fill(description);
+ await page.getByTestId('inputOfferAmount').fill(randomNonZeroNumber.toString());
+ await page.getByRole('button', { name: 'Sign & Send' }).click();
+ await expect(page.getByText('That offer was recorded.')).toBeVisible();
+
+ // go to the offer and check the values
+ await page.goto('./projects');
+ await page.locator('li').filter({ hasText: description }).locator('a').first().click();
+ await expect(page.getByRole('heading', { name: 'Verifiable Claim Details' })).toBeVisible();
+ await expect(page.getByText(description, { exact: true })).toBeVisible();
+ const serverPagePromise = page.waitForEvent('popup');
+ await page.getByRole('link', { name: 'View on the Public Server' }).click();
+ const serverPage = await serverPagePromise;
+ await serverPage.getByText(description);
+ await serverPage.getByText('did:none:HIDDEN');
+
+ // Now update that offer
+
+ // find the edit page and check the old values again
+ await page.goto('./projects');
+ await page.locator('li').filter({ hasText: description }).locator('a').first().click();
+ await page.getByTestId('editClaimButton').click();
+ await page.locator('heading', { hasText: 'What is offered' }).isVisible();
+ const itemDesc = await page.getByTestId('itemDescription');
+ await expect(itemDesc).toHaveValue(description);
+ const amount = await page.getByTestId('inputOfferAmount');
+ await expect(amount).toHaveValue(randomNonZeroNumber.toString());
+ // update the values
+ await itemDesc.fill(updatedDescription);
+ await amount.fill(String(randomNonZeroNumber + 1));
+ await page.getByRole('button', { name: 'Sign & Send' }).click();
+
+ // go to the offer claim again and check the updated values
+ await page.goto('./projects');
+ await page.locator('li').filter({ hasText: description }).locator('a').first().click();
+ const newItemDesc = await page.getByTestId('description');
+ await expect(newItemDesc).toHaveText(updatedDescription);
+
+ // go to edit page
+ await page.getByTestId('editClaimButton').click();
+ const newAmount = await page.getByTestId('inputOfferAmount');
+ await expect(newAmount).toHaveValue((randomNonZeroNumber + 1).toString());
+});
diff --git a/test-playwright/testUtils.js b/test-playwright/testUtils.ts
similarity index 80%
rename from test-playwright/testUtils.js
rename to test-playwright/testUtils.ts
index 44706ed34..7d8d52c79 100644
--- a/test-playwright/testUtils.js
+++ b/test-playwright/testUtils.ts
@@ -1,6 +1,6 @@
-import { expect } from '@playwright/test';
+import { expect, Page } from '@playwright/test';
-export async function importUser(page, id) {
+export async function importUser(page: Page, id?: string): Promise
{
let seedPhrase, userName, did;
// Set seed phrase and DID based on user ID
@@ -21,6 +21,8 @@ export async function importUser(page, id) {
await page.getByText('You have a seed').click();
await page.getByPlaceholder('Seed Phrase').fill(seedPhrase);
await page.getByRole('button', { name: 'Import' }).click();
+ await expect(page.locator('#sectionUsageLimits')).toContainText('You have done');
+ await expect(page.locator('#sectionUsageLimits')).toContainText('You have uploaded');
// Set name
await page.getByRole('link', { name: 'Set Your Name' }).click();
diff --git a/vite.config.mjs b/vite.config.mjs
index 4e60ee08f..aa55e11e0 100644
--- a/vite.config.mjs
+++ b/vite.config.mjs
@@ -16,7 +16,7 @@ export default defineConfig({
srcDir: '.',
filename: 'sw_scripts-combined.js',
manifest: {
- // This is used for the app name. It doesn't include a space, because iOS complains if i recall correctly.
+ // This is used for the app name. It doesn't include a space, because iOS complains if I recall correctly.
// There is a name with spaces in the constants/app.js file for use internally.
name: process.env.TIME_SAFARI_APP_TITLE || require('./package.json').name,
short_name: process.env.TIME_SAFARI_APP_TITLE || require('./package.json').name,