From 3f77f9b3ff9763f4903d5fa2353ef30ca0113383 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sat, 10 Aug 2024 20:09:49 -0600 Subject: [PATCH 01/24] record some info on my attempt to test a service worker --- src/registerServiceWorker.ts | 59 +++++++++---------- src/router/index.ts | 3 + .../35-record-gift-from-image-share.spec.ts | 41 +++++++++++-- 3 files changed, 67 insertions(+), 36 deletions(-) diff --git a/src/registerServiceWorker.ts b/src/registerServiceWorker.ts index 6a49858..2ad3679 100644 --- a/src/registerServiceWorker.ts +++ b/src/registerServiceWorker.ts @@ -2,33 +2,32 @@ import { register } from "register-service-worker"; -if (import.meta.env.NODE_ENV === "production") { - register("/sw_scripts-combined.js", { - ready() { - console.log( - "App is being served from cache by a service worker.\n" + - "For more details, visit https://goo.gl/AFskqB", - ); - }, - registered() { - console.log("Service worker has been registered."); - }, - cached() { - console.log("Content has been cached for offline use."); - }, - updatefound() { - console.log("New content is downloading."); - }, - updated() { - console.log("New content is available; please refresh."); - }, - offline() { - console.log( - "No internet connection found. App is running in offline mode.", - ); - }, - error(error) { - console.error("Error during service worker registration:", error); - }, - }); -} +// This used to be only done when: import.meta.env.NODE_ENV === "production" +register("/sw_scripts-combined.js", { + ready() { + console.log( + "App is being served from cache by a service worker.\n" + + "For more details, visit https://goo.gl/AFskqB", + ); + }, + registered() { + console.log("Service worker has been registered."); + }, + cached() { + console.log("Content has been cached for offline use."); + }, + updatefound() { + console.log("New content is downloading."); + }, + updated() { + console.log("New content is available; please refresh."); + }, + offline() { + console.log( + "No internet connection found. App is running in offline mode.", + ); + }, + error(error) { + console.error("Error during service worker registration:", error); + }, +}); diff --git a/src/router/index.ts b/src/router/index.ts index 2505a76..39901a6 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -189,6 +189,9 @@ const routes: Array = [ name: "shared-photo", component: () => import("@/views/SharedPhotoView.vue"), }, + + // /share-target is also an endpoint in the service worker + { path: "/start", name: "start", 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 5e9d9ec..72223e5 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'); +// }); From 60ed21c0d92b34827fe26647d17ed0bb03eebfbf Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sun, 11 Aug 2024 08:17:27 -0600 Subject: [PATCH 02/24] fix image shared with web share --- src/views/SharedPhotoView.vue | 9 +++++++++ sw_scripts/safari-notifications.js | 15 ++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/views/SharedPhotoView.vue b/src/views/SharedPhotoView.vue index 2eb3854..24f32da 100644 --- a/src/views/SharedPhotoView.vue +++ b/src/views/SharedPhotoView.vue @@ -48,6 +48,15 @@

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).

diff --git a/sw_scripts/safari-notifications.js b/sw_scripts/safari-notifications.js index 0103f5b..37a003c 100644 --- a/sw_scripts/safari-notifications.js +++ b/sw_scripts/safari-notifications.js @@ -566,14 +566,27 @@ async function getNotificationCount() { return result; } +export 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); From 5849ae2de4aa63ccd3029f234e56493799547c2a Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sun, 11 Aug 2024 19:01:34 -0600 Subject: [PATCH 03/24] remove "export" that's not available in raw JS --- sw_scripts/safari-notifications.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sw_scripts/safari-notifications.js b/sw_scripts/safari-notifications.js index 37a003c..5ba7e06 100644 --- a/sw_scripts/safari-notifications.js +++ b/sw_scripts/safari-notifications.js @@ -566,7 +566,7 @@ async function getNotificationCount() { return result; } -export async function blobToBase64String(blob) { +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? From cefa384ff1a2d922848c370640c096c529920fab Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Mon, 12 Aug 2024 09:17:32 -0600 Subject: [PATCH 04/24] bump to version 0.3.17 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d3e1ef9..bb5ce55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "TimeSafari", - "version": "0.3.17-beta", + "version": "0.3.17", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "TimeSafari", - "version": "0.3.17-beta", + "version": "0.3.17", "dependencies": { "@dicebear/collection": "^5.4.1", "@dicebear/core": "^5.4.1", diff --git a/package.json b/package.json index ffb4cae..44f1dca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "TimeSafari", - "version": "0.3.17-beta", + "version": "0.3.17", "scripts": { "dev": "vite", "serve": "vite preview", From da79d581b743f76740a6d89c27510909dd18a063 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Mon, 12 Aug 2024 09:19:15 -0600 Subject: [PATCH 05/24] bump version and add "-beta" --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index bb5ce55..cfe6ebe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "TimeSafari", - "version": "0.3.17", + "version": "0.3.18-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "TimeSafari", - "version": "0.3.17", + "version": "0.3.18-beta", "dependencies": { "@dicebear/collection": "^5.4.1", "@dicebear/core": "^5.4.1", diff --git a/package.json b/package.json index 44f1dca..441c434 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "TimeSafari", - "version": "0.3.17", + "version": "0.3.18-beta", "scripts": { "dev": "vite", "serve": "vite preview", From 089d4f07331b42e790721c5ed53ef0e0c3052e29 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Mon, 12 Aug 2024 09:23:25 -0600 Subject: [PATCH 06/24] change back the check for adding a service worker because tests would get constant errors --- src/registerServiceWorker.ts | 59 ++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/src/registerServiceWorker.ts b/src/registerServiceWorker.ts index 2ad3679..6a49858 100644 --- a/src/registerServiceWorker.ts +++ b/src/registerServiceWorker.ts @@ -2,32 +2,33 @@ import { register } from "register-service-worker"; -// This used to be only done when: import.meta.env.NODE_ENV === "production" -register("/sw_scripts-combined.js", { - ready() { - console.log( - "App is being served from cache by a service worker.\n" + - "For more details, visit https://goo.gl/AFskqB", - ); - }, - registered() { - console.log("Service worker has been registered."); - }, - cached() { - console.log("Content has been cached for offline use."); - }, - updatefound() { - console.log("New content is downloading."); - }, - updated() { - console.log("New content is available; please refresh."); - }, - offline() { - console.log( - "No internet connection found. App is running in offline mode.", - ); - }, - error(error) { - console.error("Error during service worker registration:", error); - }, -}); +if (import.meta.env.NODE_ENV === "production") { + register("/sw_scripts-combined.js", { + ready() { + console.log( + "App is being served from cache by a service worker.\n" + + "For more details, visit https://goo.gl/AFskqB", + ); + }, + registered() { + console.log("Service worker has been registered."); + }, + cached() { + console.log("Content has been cached for offline use."); + }, + updatefound() { + console.log("New content is downloading."); + }, + updated() { + console.log("New content is available; please refresh."); + }, + offline() { + console.log( + "No internet connection found. App is running in offline mode.", + ); + }, + error(error) { + console.error("Error during service worker registration:", error); + }, + }); +} From 1fe540d5a8915191216f60a851707e0aa7087e3f Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Mon, 12 Aug 2024 09:25:01 -0600 Subject: [PATCH 07/24] fix list of offers (and some other lists), and add tests for offers --- CHANGELOG.md | 7 ++++- playwright.config-local.ts | 4 +-- src/components/OfferDialog.vue | 2 ++ src/views/AccountViewView.vue | 17 +++++------- src/views/ClaimView.vue | 5 +++- src/views/ContactAmountsView.vue | 2 +- src/views/DiscoverView.vue | 2 ++ src/views/NewEditProjectView.vue | 2 +- src/views/ProjectViewView.vue | 1 + src/views/ProjectsView.vue | 8 +++--- src/views/SharedPhotoView.vue | 8 +++--- test-playwright/20-create-project.spec.ts | 2 -- test-playwright/30-record-gift.spec.ts | 2 +- test-playwright/40-add-contact.spec.ts | 2 +- test-playwright/50-record-offer.spec.ts | 33 +++++++++++++++++++++++ test-playwright/testUtils.js | 2 ++ 16 files changed, 71 insertions(+), 28 deletions(-) create mode 100644 test-playwright/50-record-offer.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f72edf..1650cf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.3.16] - 2024.07.10 +## [0.3.18] - 2024.07.12 +### Fixed +- List of offers wasn't showing. + + +## [0.3.17] - 2024.07.11 - cefa384ff1a2d922848c370640c096c529920fab ### Added - Photos on more screens ### Fixed diff --git a/playwright.config-local.ts b/playwright.config-local.ts index e7647e3..ff9f647 100644 --- a/playwright.config-local.ts +++ b/playwright.config-local.ts @@ -72,9 +72,9 @@ export default defineConfig({ }, ], - /* Configure global timeout */ + /* Configure global timeout; default is 30000 milliseconds */ // the image upload will often not succeed at 5 seconds - //timeout: 7000, + //timeout: 10000, /* Run your local dev server before starting the tests */ /** diff --git a/src/components/OfferDialog.vue b/src/components/OfferDialog.vue index 1498c06..af48780 100644 --- a/src/components/OfferDialog.vue +++ b/src/components/OfferDialog.vue @@ -4,6 +4,7 @@

Offer Help

- {{ veriClaim.claim?.description }} + {{ + veriClaim.claim?.description || + veriClaim.claim?.itemOffered?.description + }}
diff --git a/src/views/ContactAmountsView.vue b/src/views/ContactAmountsView.vue index 98c955b..0b3a0a9 100644 --- a/src/views/ContactAmountsView.vue +++ b/src/views/ContactAmountsView.vue @@ -271,7 +271,7 @@ export default class ContactAmountssView extends Vue { // Make the xhr request payload const payload = JSON.stringify({ jwtEncoded: vcJwt }); const url = this.apiServer + "/api/v2/claim"; - const headers = getHeaders(this.activeDid) as AxiosRequestHeaders; + const headers = (await getHeaders(this.activeDid)) as AxiosRequestHeaders; try { const resp = await this.axios.post(url, payload, { headers }); diff --git a/src/views/DiscoverView.vue b/src/views/DiscoverView.vue index 4f73a83..61b0669 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/NewEditProjectView.vue b/src/views/NewEditProjectView.vue index 6d5f4d0..5c469f0 100644 --- a/src/views/NewEditProjectView.vue +++ b/src/views/NewEditProjectView.vue @@ -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/ProjectViewView.vue b/src/views/ProjectViewView.vue index 242960a..0f4d185 100644 --- a/src/views/ProjectViewView.vue +++ b/src/views/ProjectViewView.vue @@ -162,6 +162,7 @@
diff --git a/test-playwright/20-create-project.spec.ts b/test-playwright/20-create-project.spec.ts index af22ff0..d7742bc 100644 --- a/test-playwright/20-create-project.spec.ts +++ b/test-playwright/20-create-project.spec.ts @@ -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 387be37..77dc3ff 100644 --- a/test-playwright/30-record-gift.spec.ts +++ b/test-playwright/30-record-gift.spec.ts @@ -27,7 +27,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/40-add-contact.spec.ts b/test-playwright/40-add-contact.spec.ts index f625474..4bb7656 100644 --- a/test-playwright/40-add-contact.spec.ts +++ b/test-playwright/40-add-contact.spec.ts @@ -51,7 +51,7 @@ test('Add contact, record gift, confirm gift', async ({ page }) => { // Record something given by new contact await page.getByRole('heading', { name: contactName }).click(); await page.getByPlaceholder('What was given').fill(finalTitle); - await page.getByRole('spinbutton').fill(randomNonZeroNumber.toString()); + await page.getByRole('spinbutton', { id: 'inputGivenAmount' }).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/50-record-offer.spec.ts b/test-playwright/50-record-offer.spec.ts new file mode 100644 index 0000000..031c99f --- /dev/null +++ b/test-playwright/50-record-offer.spec.ts @@ -0,0 +1,33 @@ +import { test, expect } from '@playwright/test'; +import { importUser } from './testUtils'; + +test('Record an offer', async ({ page }) => { + // Generate a random string of 6 characters, skipping the "0." at the beginning + const randomString = Math.random().toString(36).substring(2, 8); + // Standard title prefix + const finalTitle = `Offer ${randomString}`; + const randomNonZeroNumber = Math.floor(Math.random() * 999) + 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(finalTitle); + await page.getByTestId('inputOfferAmount').fill(randomNonZeroNumber.toString()); + await page.getByRole('button', { name: 'Sign & Send' }).click(); + await expect(page.getByText('That offer was recorded.')).toBeVisible(); + + // Refresh home view and check gift + await page.goto('./projects'); + await page.locator('li').filter({ hasText: `All ${randomNonZeroNumber} remaining` }).locator('a').first().click(); + await expect(page.getByRole('heading', { name: 'Verifiable Claim Details' })).toBeVisible(); + await expect(page.getByText(finalTitle, { exact: true })).toBeVisible(); + const page1Promise = page.waitForEvent('popup'); + await page.getByRole('link', { name: 'View on the Public Server' }).click(); + const page1 = await page1Promise; +}); \ No newline at end of file diff --git a/test-playwright/testUtils.js b/test-playwright/testUtils.js index 44706ed..3cd2e5c 100644 --- a/test-playwright/testUtils.js +++ b/test-playwright/testUtils.js @@ -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(); From 56e34408755d1b14d82b921e991492fdf0dff035 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Mon, 12 Aug 2024 18:51:41 -0600 Subject: [PATCH 08/24] misc commentary --- CHANGELOG.md | 2 +- README.md | 2 ++ playwright.config-local.ts | 2 +- src/registerServiceWorker.ts | 1 + vite.config.mjs | 2 +- 5 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1650cf7..3413c4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.3.18] - 2024.07.12 +## ? ### Fixed - List of offers wasn't showing. diff --git a/README.md b/README.md index c4350bc..c64fd08 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ npm run lint * Update the ClickUp tasks & CHANGELOG.md & the version in package.json, run `npm install`. +* Commit everything (since the commit hash is used the app). + * Record what version is currently on production. * Run the correct build: diff --git a/playwright.config-local.ts b/playwright.config-local.ts index ff9f647..02f0ae3 100644 --- a/playwright.config-local.ts +++ b/playwright.config-local.ts @@ -86,7 +86,7 @@ export default defineConfig({ * }, * * But if we do then the testInfo.config.webServer is null and the API-setting test 00 fails. - * It is worth considering a change such that Time Safari's default Endorer API server is NOT set + * It is worth considering a change such that Time Safari's default Endorser API server is NOT set * in the user's settings so that it can be blanked out and the default is used. */ webServer: { diff --git a/src/registerServiceWorker.ts b/src/registerServiceWorker.ts index 6a49858..ba7d8a5 100644 --- a/src/registerServiceWorker.ts +++ b/src/registerServiceWorker.ts @@ -2,6 +2,7 @@ import { register } from "register-service-worker"; +// NODE_ENV is "production" by default with "vite build". See https://vitejs.dev/guide/env-and-mode if (import.meta.env.NODE_ENV === "production") { register("/sw_scripts-combined.js", { ready() { diff --git a/vite.config.mjs b/vite.config.mjs index 4e60ee0..aa55e11 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, From 4244e6b279dfda504f710ad3dcc07b7c99f7ba67 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Mon, 12 Aug 2024 20:38:54 -0600 Subject: [PATCH 09/24] add recipient description to offers in user's list --- CONTRIBUTING.md | 9 +++- src/libs/endorserServer.ts | 8 ++-- src/views/ProjectsView.vue | 62 +++++++++++++++++++++---- test-playwright/50-record-offer.spec.ts | 4 +- 4 files changed, 66 insertions(+), 17 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 26c0388..c7885c5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,5 +2,10 @@ Welcome! We are happy to have your help with this project. -Note that all contributions will be under our -[license, modeled after SQLite](https://github.com/trentlarson/endorser-ch/blob/master/LICENSE). +We expect contributions to include automated tests and pass linting. Run the `test-all` task. +Note that some previous features don't have tests and adding more will make you friends quick. + +Note that all contributions will be under our [license, modeled after SQLite](https://github.com/trentlarson/endorser-ch/blob/master/LICENSE). + +If you want to see a code of conduct, we're probably not the people you want to hang with. +Basically, we'll work together as long as we both enjoy it, and we'll stop when that stops. diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index 97b943b..d7475a8 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -267,10 +267,6 @@ export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult; // See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6 const HIDDEN_DID = "did:none:HIDDEN"; -const planCache: LRUCache = new LRUCache({ - max: 500, -}); - export function isDid(did: string) { return did.startsWith("did:"); } @@ -507,6 +503,10 @@ export async function getHeaders(did?: string) { return headers; } +const planCache: LRUCache = new LRUCache({ + max: 500, +}); + /** * @param handleId nullable, in which case "undefined" will be returned * @param requesterDid optional, in which case no private info will be returned diff --git a/src/views/ProjectsView.vue b/src/views/ProjectsView.vue index 165a85e..d939477 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); } @@ -402,7 +422,31 @@ export default class ProjectsView extends Vue { 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:", diff --git a/test-playwright/50-record-offer.spec.ts b/test-playwright/50-record-offer.spec.ts index 031c99f..c3d5aac 100644 --- a/test-playwright/50-record-offer.spec.ts +++ b/test-playwright/50-record-offer.spec.ts @@ -5,7 +5,7 @@ test('Record an offer', async ({ page }) => { // Generate a random string of 6 characters, skipping the "0." at the beginning const randomString = Math.random().toString(36).substring(2, 8); // Standard title prefix - const finalTitle = `Offer ${randomString}`; + const finalTitle = `Offering of ${randomString}`; const randomNonZeroNumber = Math.floor(Math.random() * 999) + 1; // Create new ID for default user @@ -24,7 +24,7 @@ test('Record an offer', async ({ page }) => { // Refresh home view and check gift await page.goto('./projects'); - await page.locator('li').filter({ hasText: `All ${randomNonZeroNumber} remaining` }).locator('a').first().click(); + await page.locator('li').filter({ hasText: finalTitle }).locator('a').first().click(); await expect(page.getByRole('heading', { name: 'Verifiable Claim Details' })).toBeVisible(); await expect(page.getByText(finalTitle, { exact: true })).toBeVisible(); const page1Promise = page.waitForEvent('popup'); From 2c2c95a824bcbc02d07d8e9c184a4ff01474fdff Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Wed, 14 Aug 2024 08:56:57 -0600 Subject: [PATCH 10/24] fix destination page after photo is shared --- src/views/SharedPhotoView.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/SharedPhotoView.vue b/src/views/SharedPhotoView.vue index cf32e46..1bd583e 100644 --- a/src/views/SharedPhotoView.vue +++ b/src/views/SharedPhotoView.vue @@ -133,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, From 05f898d462f04ad9f2b84f3c7925d9ea18259588 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Thu, 15 Aug 2024 19:41:18 -0600 Subject: [PATCH 11/24] put BTC before BX in unit rotation --- src/libs/util.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/util.ts b/src/libs/util.ts index 77ea3e3..a40f4c4 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -30,8 +30,8 @@ export const SHARED_PHOTO_BASE64_KEY = "shared-photo-base64"; /* eslint-disable prettier/prettier */ export const UNIT_SHORT: Record = { - "BX": "BX", "BTC": "BTC", + "BX": "BX", "ETH": "ETH", "HUR": "Hours", "USD": "US $", @@ -40,8 +40,8 @@ export const UNIT_SHORT: Record = { /* eslint-disable prettier/prettier */ export const UNIT_LONG: Record = { - "BX": "Buxbe", "BTC": "Bitcoin", + "BX": "Buxbe", "ETH": "Ethereum", "HUR": "hours", "USD": "dollars", From 269d00a096cd8f70961b42ede4ff7a05e4a287c3 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 16 Aug 2024 15:58:54 -0600 Subject: [PATCH 12/24] start with offer-edit --- src/libs/endorserServer.ts | 168 +++++++--- src/views/GiftedDetails.vue | 1 - src/views/OfferDetails.vue | 624 ++++++++++++++++++++++++++++++++++++ 3 files changed, 748 insertions(+), 45 deletions(-) create mode 100644 src/views/OfferDetails.vue diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index d7475a8..619afa1 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -48,7 +48,7 @@ export interface ClaimResult { } export interface GenericVerifiableCredential { - "@context"?: string; + "@context"?: string; // optional when embedded, eg. in an Agree "@type": string; [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any } @@ -139,13 +139,13 @@ export interface GiveVerifiableCredential extends GenericVerifiableCredential { // Note that previous VCs may have additional fields. // https://endorser.ch/doc/html/transactions.html#id8 -export interface OfferVerifiableCredential { - "@context"?: string; // optional when embedded, eg. in an Agree +export interface OfferVerifiableCredential extends GenericVerifiableCredential { + "@context"?: string; // optional when embedded... though it doesn't make sense to agree to an offer "@type": "Offer"; - description?: string; + description?: string; // conditions for the offer includesObject?: { amountOfThisGood: number; unitCode: string }; itemOffered?: { - description?: string; + description?: string; // description of the item isPartOf?: { identifier?: string; lastClaimId?: string; "@type"?: string }; }; offeredBy?: { identifier: string }; @@ -155,7 +155,7 @@ export interface OfferVerifiableCredential { // Note that previous VCs may have additional fields. // https://endorser.ch/doc/html/transactions.html#id7 -export interface PlanVerifiableCredential { +export interface PlanVerifiableCredential extends GenericVerifiableCredential { "@context": "https://schema.org"; "@type": "PlanAction"; name: string; @@ -563,6 +563,8 @@ export async function setPlanInCache( /** * Construct GiveAction VC for submission to server + * + * @param lastClaimId supplied when editing a previous claim */ export function hydrateGive( vcClaimOrig?: GiveVerifiableCredential, @@ -587,6 +589,7 @@ export function hydrateGive( }; if (lastClaimId) { + // this is an edit vcClaim.lastClaimId = lastClaimId; delete vcClaim.identifier; } @@ -594,16 +597,17 @@ export function hydrateGive( vcClaim.agent = fromDid ? { identifier: fromDid } : undefined; vcClaim.recipient = toDid ? { identifier: toDid } : undefined; vcClaim.description = description || undefined; - vcClaim.object = amount - ? { amountOfThisGood: amount, unitCode: unitCode || "HUR" } - : undefined; + vcClaim.object = + amount && !isNaN(amount) + ? { amountOfThisGood: amount, unitCode: unitCode || "HUR" } + : undefined; // ensure fulfills is an array if (!Array.isArray(vcClaim.fulfills)) { vcClaim.fulfills = vcClaim.fulfills ? [vcClaim.fulfills] : []; } // ... and replace or add each element, ending with Trade or Donate - // I realize this doesn't change any elements that are not PlanAction or Offer or Trade/Action. + // I realize the following doesn't change any elements that are not PlanAction or Offer or Trade/Action. vcClaim.fulfills = vcClaim.fulfills.filter( (elem) => elem["@type"] !== "PlanAction", ); @@ -639,8 +643,8 @@ export function hydrateGive( * * @param fromDid may be null * @param toDid - * @param description may be null; should have this or amount - * @param amount may be null; should have this or description + * @param description may be null + * @param amount may be null */ export async function createAndSubmitGive( axios: Axios, @@ -667,6 +671,7 @@ export async function createAndSubmitGive( fulfillsOfferHandleId, isTrade, imageUrl, + undefined, ); return createAndSubmitClaim( vcClaim as GenericVerifiableCredential, @@ -680,9 +685,9 @@ export async function createAndSubmitGive( * For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim * * @param fromDid may be null - * @param toDid - * @param description may be null; should have this or amount - * @param amount may be null; should have this or description + * @param toDid may be null if project is provided + * @param description may be null + * @param amount may be null */ export async function editAndSubmitGive( axios: Axios, @@ -720,13 +725,69 @@ export async function editAndSubmitGive( ); } +/** + * Construct Offer VC for submission to server + * + * @param lastClaimId supplied when editing a previous claim + */ +export function hydrateOffer( + vcClaimOrig?: OfferVerifiableCredential, + fromDid?: string, + toDid?: string, + conditionDescription?: string, + amount?: number, + unitCode?: string, + offeringDescription?: string, + fulfillsProjectHandleId?: string, + validThrough?: string, + lastClaimId?: string, +): OfferVerifiableCredential { + // Remember: replace values or erase if it's null + + const vcClaim: OfferVerifiableCredential = vcClaimOrig + ? R.clone(vcClaimOrig) + : { + "@context": SCHEMA_ORG_CONTEXT, + "@type": "Offer", + }; + + if (lastClaimId) { + // this is an edit + vcClaim.lastClaimId = lastClaimId; + delete vcClaim.identifier; + } + + vcClaim.offeredBy = fromDid ? { identifier: fromDid } : undefined; + vcClaim.recipient = toDid ? { identifier: toDid } : undefined; + vcClaim.description = conditionDescription || undefined; + + vcClaim.includesObject = + amount && !isNaN(amount) + ? { amountOfThisGood: amount, unitCode: unitCode || "HUR" } + : undefined; + + if (offeringDescription || fulfillsProjectHandleId) { + vcClaim.itemOffered = vcClaim.itemOffered || {}; + vcClaim.itemOffered.description = offeringDescription || undefined; + if (fulfillsProjectHandleId) { + vcClaim.itemOffered.isPartOf = { + "@type": "PlanAction", + identifier: fulfillsProjectHandleId, + }; + } + } + vcClaim.validThrough = validThrough || undefined; + + return vcClaim; +} + /** * For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim * * @param identity - * @param description may be null; should have this or amount - * @param amount may be null; should have this or description - * @param expirationDate ISO 8601 date string YYYY-MM-DD (may be null) + * @param description may be null + * @param amount may be null + * @param validThrough ISO 8601 date string YYYY-MM-DD (may be null) * @param fulfillsProjectHandleId ID of project to which this contributes (may be null) */ export async function createAndSubmitOffer( @@ -736,35 +797,54 @@ export async function createAndSubmitOffer( description?: string, amount?: number, unitCode?: string, - expirationDate?: string, + validThrough?: string, recipientDid?: string, fulfillsProjectHandleId?: string, ): Promise { - const vcClaim: OfferVerifiableCredential = { - "@context": SCHEMA_ORG_CONTEXT, - "@type": "Offer", - offeredBy: { identifier: issuerDid }, - validThrough: expirationDate || undefined, - }; - if (amount) { - vcClaim.includesObject = { - amountOfThisGood: amount, - unitCode: unitCode || "HUR", - }; - } - if (description) { - vcClaim.itemOffered = { description }; - } - if (recipientDid) { - vcClaim.recipient = { identifier: recipientDid }; - } - if (fulfillsProjectHandleId) { - vcClaim.itemOffered = vcClaim.itemOffered || {}; - vcClaim.itemOffered.isPartOf = { - "@type": "PlanAction", - identifier: fulfillsProjectHandleId, - }; - } + const vcClaim = hydrateOffer( + undefined, + issuerDid, + recipientDid, + description, + amount, + unitCode, + description, + fulfillsProjectHandleId, + validThrough, + undefined, + ); + return createAndSubmitClaim( + vcClaim as OfferVerifiableCredential, + issuerDid, + apiServer, + axios, + ); +} + +export async function editAndSubmitOffer( + axios: Axios, + apiServer: string, + fullClaim: GenericCredWrapper, + issuerDid: string, + description?: string, + amount?: number, + unitCode?: string, + validThrough?: string, + recipientDid?: string, + fulfillsProjectHandleId?: string, +): Promise { + const vcClaim = hydrateOffer( + fullClaim.claim, + issuerDid, + recipientDid, + description, + amount, + unitCode, + description, + fulfillsProjectHandleId, + validThrough, + fullClaim.id, + ); return createAndSubmitClaim( vcClaim as OfferVerifiableCredential, issuerDid, diff --git a/src/views/GiftedDetails.vue b/src/views/GiftedDetails.vue index 3e1466c..c727bdf 100644 --- a/src/views/GiftedDetails.vue +++ b/src/views/GiftedDetails.vue @@ -694,7 +694,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/OfferDetails.vue b/src/views/OfferDetails.vue new file mode 100644 index 0000000..f6a1225 --- /dev/null +++ b/src/views/OfferDetails.vue @@ -0,0 +1,624 @@ +