From b8181f6ae3a023d9b22d13a974d76b87e4e6e469 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Thu, 8 Aug 2024 08:51:25 -0600 Subject: [PATCH] fix error sharing image and failing to upload, fix upload in webkit/safari, and test it --- playwright.config-local.ts | 3 +- src/components/PhotoDialog.vue | 1 + src/db/tables/temp.ts | 3 +- src/libs/util.ts | 33 +++++++++++++++ src/views/GiftedDetails.vue | 2 +- src/views/HomeView.vue | 5 ++- src/views/SharedPhotoView.vue | 18 ++++++--- src/views/TestView.vue | 12 ++++-- .../35-record-gift-from-image-share.spec.ts | 40 +++++++++++++++++++ 9 files changed, 104 insertions(+), 13 deletions(-) create mode 100644 test-playwright/35-record-gift-from-image-share.spec.ts diff --git a/playwright.config-local.ts b/playwright.config-local.ts index fa158aa..e7647e3 100644 --- a/playwright.config-local.ts +++ b/playwright.config-local.ts @@ -73,7 +73,8 @@ export default defineConfig({ ], /* Configure global timeout */ - //pageTimeout: 30000, + // the image upload will often not succeed at 5 seconds + //timeout: 7000, /* Run your local dev server before starting the tests */ /** diff --git a/src/components/PhotoDialog.vue b/src/components/PhotoDialog.vue index fe323cb..015d3da 100644 --- a/src/components/PhotoDialog.vue +++ b/src/components/PhotoDialog.vue @@ -350,6 +350,7 @@ export default class PhotoDialog extends Vue { const token = await accessToken(this.activeDid); const headers = { Authorization: "Bearer " + token, + // axios fills in Content-Type of multipart/form-data }; const formData = new FormData(); if (!this.blob) { diff --git a/src/db/tables/temp.ts b/src/db/tables/temp.ts index 02b592b..0c77f56 100644 --- a/src/db/tables/temp.ts +++ b/src/db/tables/temp.ts @@ -2,7 +2,8 @@ export type Temp = { id: string; - blob?: Blob; + blob?: Blob; // deprecated because webkit (Safari) does not support Blob + blobB64?: string; // base64-encoded blob }; /** diff --git a/src/libs/util.ts b/src/libs/util.ts index 91e1c3b..77ea3e3 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -26,6 +26,7 @@ import { createPeerDid } from "@/libs/crypto/vc/didPeer"; export const PRIVACY_MESSAGE = "The data you send will be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to them and others you explicitly allow."; +export const SHARED_PHOTO_BASE64_KEY = "shared-photo-base64"; /* eslint-disable prettier/prettier */ export const UNIT_SHORT: Record = { @@ -116,6 +117,38 @@ export const isGiveRecordTheUserCanConfirm = ( ); }; +export async function blobToBase64(blob: Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result as string); // potential problem if it returns an ArrayBuffer? + reader.onerror = reject; + reader.readAsDataURL(blob); + }); +} + +export function base64ToBlob(base64DataUrl: string, sliceSize = 512) { + // Extract the content type and the Base64 data + const [metadata, base64] = base64DataUrl.split(","); + const contentTypeMatch = metadata.match(/data:(.*?);base64/); + const contentType = contentTypeMatch ? contentTypeMatch[1] : ""; + + const byteCharacters = atob(base64); + const byteArrays = []; + + for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) { + const slice = byteCharacters.slice(offset, offset + sliceSize); + + const byteNumbers = new Array(slice.length); + for (let i = 0; i < slice.length; i++) { + byteNumbers[i] = slice.charCodeAt(i); + } + + const byteArray = new Uint8Array(byteNumbers); + byteArrays.push(byteArray); + } + return new Blob(byteArrays, { type: contentType }); +} + /** * @returns the DID of the person who offered, or undefined if hidden * @param veriClaim is expected to have fields: claim and issuer diff --git a/src/views/GiftedDetails.vue b/src/views/GiftedDetails.vue index 6f6c374..7658714 100644 --- a/src/views/GiftedDetails.vue +++ b/src/views/GiftedDetails.vue @@ -64,7 +64,7 @@ -
+ -
+
diff --git a/src/views/SharedPhotoView.vue b/src/views/SharedPhotoView.vue index 7779d1c..2eb3854 100644 --- a/src/views/SharedPhotoView.vue +++ b/src/views/SharedPhotoView.vue @@ -66,7 +66,8 @@ import { } from "@/constants/app"; import { db } from "@/db/index"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; -import { getHeaders } from "@/libs/endorserServer"; +import { accessToken } from "@/libs/crypto"; +import { base64ToBlob, SHARED_PHOTO_BASE64_KEY } from "@/libs/util"; @Component({ components: { PhotoDialog, QuickNav } }) export default class SharedPhotoView extends Vue { @@ -86,16 +87,19 @@ export default class SharedPhotoView extends Vue { const settings = await db.settings.get(MASTER_SETTINGS_KEY); this.activeDid = settings?.activeDid as string; - const temp = await db.temp.get("shared-photo"); + const temp = await db.temp.get(SHARED_PHOTO_BASE64_KEY); + const imageB64 = temp?.blobB64 as string; if (temp) { - this.imageBlob = temp.blob; + this.imageBlob = base64ToBlob(imageB64); // clear the temp image - db.temp.delete("shared-photo"); + db.temp.delete(SHARED_PHOTO_BASE64_KEY); this.imageFileName = (this.$route as Router).query[ "fileName" ] as string; + } else { + console.error("No appropriate image found in temp storage.", temp); } } catch (err: unknown) { console.error("Got an error loading an identifier:", err); @@ -156,7 +160,11 @@ export default class SharedPhotoView extends Vue { let result; try { // send the image to the server - const headers = await getHeaders(this.activeDid); + const token = await accessToken(this.activeDid); + const headers = { + Authorization: "Bearer " + token, + // axios fills in Content-Type of multipart/form-data + }; const formData = new FormData(); formData.append( "image", diff --git a/src/views/TestView.vue b/src/views/TestView.vue index 4570f0e..6461cf9 100644 --- a/src/views/TestView.vue +++ b/src/views/TestView.vue @@ -157,7 +157,7 @@

Image Sharing

Populates the "shared-photo" view as if they used "share_target". - + Go to Shared Page @@ -257,8 +258,10 @@ import { } from "@/libs/crypto/vc/passkeyDidPeer"; import { AccountKeyInfo, + blobToBase64, getAccount, registerAndSavePasskey, + SHARED_PHOTO_BASE64_KEY, } from "@/libs/util"; const inputFileNameRef = ref(); @@ -320,12 +323,13 @@ export default class Help extends Vue { const blob = new Blob([new Uint8Array(data)], { type: file.type, }); + const blobB64 = await blobToBase64(blob); this.fileName = file.name as string; - const temp = await db.temp.get("shared-photo"); + const temp = await db.temp.get(SHARED_PHOTO_BASE64_KEY); if (temp) { - await db.temp.update("shared-photo", { blob }); + await db.temp.update(SHARED_PHOTO_BASE64_KEY, { blobB64 }); } else { - await db.temp.add({ id: "shared-photo", blob }); + await db.temp.add({ id: SHARED_PHOTO_BASE64_KEY, blobB64 }); } } }; diff --git a/test-playwright/35-record-gift-from-image-share.spec.ts b/test-playwright/35-record-gift-from-image-share.spec.ts new file mode 100644 index 0000000..5e9d9ec --- /dev/null +++ b/test-playwright/35-record-gift-from-image-share.spec.ts @@ -0,0 +1,40 @@ +import path from 'path'; +import { test, expect } from '@playwright/test'; + +test('Record item given from image-share', async ({ page }) => { + + let randomString = Math.random().toString(36).substring(2, 8); + + // 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(); + + // Record something given + await page.goto('./test'); + + const fileChooserPromise = page.waitForEvent('filechooser'); + await page.getByTestId('fileInput').click(); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles(path.join(__dirname, '..', 'public', 'img', 'icons', 'android-chrome-192x192.png')); + await page.getByTestId('fileUploadButton').click(); + + // on shared photo page, choose the gift option + await page.getByRole('button').filter({ hasText: /gift/i }).click(); + + await page.getByTestId('imagery').getByRole('img').isVisible(); + await page.getByPlaceholder('What was received').fill(finalTitle); + await page.getByRole('spinbutton').fill('2'); + await page.getByRole('button', { name: 'Sign & Send' }).click(); + await expect(page.getByText('That gift was recorded.')).toBeVisible(); + + + // Refresh home view and check gift + await page.goto('./'); + const item1 = page.locator('li').filter({ hasText: finalTitle }); + await expect(item1.getByRole('img')).toBeVisible(); +}); \ No newline at end of file