diff --git a/src/App.vue b/src/App.vue index 2ec9239..50249b2 100644 --- a/src/App.vue +++ b/src/App.vue @@ -309,6 +309,28 @@ + +
+
+
+

+ Something has gone very wrong. We'd appreciate if you'd + contact us and let us know how you got here. Thank you! +

+ +
+
+
diff --git a/src/components/PushNotificationPermission.vue b/src/components/PushNotificationPermission.vue index dd9d283..54af4f8 100644 --- a/src/components/PushNotificationPermission.vue +++ b/src/components/PushNotificationPermission.vue @@ -9,7 +9,7 @@ >
{ - logConsoleAndDb( - "Requesting permission for notifications: " + JSON.stringify(navigator), - ); + // console.log( + // "Requesting permission for notifications: " + JSON.stringify(navigator), + // ); if ( !("serviceWorker" in navigator && navigator.serviceWorker?.controller) ) { @@ -344,7 +344,7 @@ export default class PushNotificationPermission extends Vue { }, -1, ); - throw new Error("We weren't granted permission."); + throw new Error("Permission was not granted to this app."); } return permission; }, diff --git a/src/components/QuickNav.vue b/src/components/QuickNav.vue index 58b02c2..ade2161 100644 --- a/src/components/QuickNav.vue +++ b/src/components/QuickNav.vue @@ -90,6 +90,12 @@ >
+ profile
diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index fedf436..ed44c39 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -4,10 +4,10 @@ import { sha256 } from "ethereum-cryptography/sha256"; import { LRUCache } from "lru-cache"; import * as R from "ramda"; -import { DEFAULT_IMAGE_API_SERVER } from "@/constants/app"; +import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app"; import { Contact } from "@/db/tables/contacts"; import { accessToken, deriveAddress, nextDerivationPath } from "@/libs/crypto"; -import { NonsensitiveDexie } from "@/db/index"; +import { logConsoleAndDb, NonsensitiveDexie } from "@/db/index"; import { retrieveAccountMetadata, retrieveFullyDecryptedAccount, @@ -501,35 +501,72 @@ export function tokenExpiryTimeDescription() { /** * Get the headers for a request, potentially including Authorization */ -export async function getHeaders(did?: string) { +export async function getHeaders( + did?: string, + $notify?: (notification: NotificationIface, timeout?: number) => void, + failureMessage?: string, +) { const headers: { "Content-Type": string; Authorization?: string } = { "Content-Type": "application/json", }; if (did) { - let token; - const account = await retrieveAccountMetadata(did); - if (account?.passkeyCredIdHex) { - if ( - passkeyAccessToken && - passkeyTokenExpirationEpochSeconds > Date.now() / 1000 - ) { - // there's an active current passkey token - token = passkeyAccessToken; + try { + let token; + const account = await retrieveAccountMetadata(did); + if (account?.passkeyCredIdHex) { + if ( + passkeyAccessToken && + passkeyTokenExpirationEpochSeconds > Date.now() / 1000 + ) { + // there's an active current passkey token + token = passkeyAccessToken; + } else { + // there's no current passkey token or it's expired + token = await accessToken(did); + + passkeyAccessToken = token; + const passkeyExpirationSeconds = await getPasskeyExpirationSeconds(); + passkeyTokenExpirationEpochSeconds = + Date.now() / 1000 + passkeyExpirationSeconds; + } } else { - // there's no current passkey token or it's expired token = await accessToken(did); - - passkeyAccessToken = token; - const passkeyExpirationSeconds = await getPasskeyExpirationSeconds(); - passkeyTokenExpirationEpochSeconds = - Date.now() / 1000 + passkeyExpirationSeconds; } - } else { - token = await accessToken(did); + headers["Authorization"] = "Bearer " + token; + } catch (error) { + // This rarely happens: we've seen it when they have account info but the + // encryption secret got lost. But in most cases we want users to at + // least see their feed -- and anything else that returns results for + // anonymous users. + + // We'll continue with an anonymous request... still want to show feed and other things, but ideally let them know. + logConsoleAndDb( + "Something failed in getHeaders call (will proceed anonymously" + + ($notify ? " and notify user" : "") + + "): " + + // IntelliJ type system complains about getCircularReplacer() with: Argument of type '(obj: any, key: string, value: any) => any' is not assignable to parameter of type '(this: any, key: string, value: any) => any'. + //JSON.stringify(error, getCircularReplacer()), // JSON.stringify(error) on a Dexie error throws another error about: Converting circular structure to JSON + error, + true, + ); + if ($notify) { + // remember: only want to do this if they supplied a DID, expecting personal results + const notifyMessage = + failureMessage || + "Showing anonymous data. See the Help page for help with personal data."; + $notify( + { + group: "alert", + type: "danger", + title: "Personal Data Error", + text: notifyMessage, + }, + 3000, + ); + } } - headers["Authorization"] = "Bearer " + token; } else { - // it's often OK to request without auth; we assume necessary checks are done earlier + // it's usually OK to request without auth; we assume we're only here when allowed } return headers; } @@ -611,6 +648,7 @@ export async function getNewOffersToUser( url += "&beforeId=" + beforeOfferJwtId; } const headers = await getHeaders(activeDid); + console.log("Using headers: ", headers); const response = await axios.get(url, { headers }); return response.data; } diff --git a/src/libs/util.ts b/src/libs/util.ts index f449cbe..653e784 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -147,6 +147,23 @@ export interface ConfirmerData { numConfsNotVisible: number; } +// // This is meant to be a second argument to JSON.stringify to avoid circular references. +// // Usage: JSON.stringify(error, getCircularReplacer()) +// // Beware: we've seen this return "undefined" when there is actually a message, eg: DatabaseClosedError: Error DEXIE ENCRYPT ADDON: Encryption key has changed +// function getCircularReplacer() { +// const seen = new WeakSet(); +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// return (obj: any, key: string, value: any): any => { +// if (typeof value === "object" && value !== null) { +// if (seen.has(value)) { +// return "[circular ref]"; +// } +// seen.add(value); +// } +// return value; +// }; +// } + /** * @return only confirmers, excluding the issuer and hidden DIDs */ @@ -423,11 +440,13 @@ export function findAllVisibleToDids( export interface AccountKeyInfo extends Account, KeyMeta {} export const retrieveAccountCount = async (): Promise => { + // one of the few times we use accountsDBPromise directly; try to avoid more usage const accountsDB = await accountsDBPromise; return await accountsDB.accounts.count(); }; export const retrieveAccountDids = async (): Promise => { + // one of the few times we use accountsDBPromise directly; try to avoid more usage const accountsDB = await accountsDBPromise; const allAccounts = await accountsDB.accounts.toArray(); const allDids = allAccounts.map((acc) => acc.did); @@ -439,6 +458,7 @@ export const retrieveAccountDids = async (): Promise => { export const retrieveAccountMetadata = async ( activeDid: string, ): Promise => { + // one of the few times we use accountsDBPromise directly; try to avoid more usage const accountsDB = await accountsDBPromise; const account = (await accountsDB.accounts .where("did") @@ -454,6 +474,7 @@ export const retrieveAccountMetadata = async ( }; export const retrieveAllAccountsMetadata = async (): Promise => { + // one of the few times we use accountsDBPromise directly; try to avoid more usage const accountsDB = await accountsDBPromise; const array = await accountsDB.accounts.toArray(); return array.map((account) => { @@ -466,6 +487,7 @@ export const retrieveAllAccountsMetadata = async (): Promise => { export const retrieveFullyDecryptedAccount = async ( activeDid: string, ): Promise => { + // one of the few times we use accountsDBPromise directly; try to avoid more usage const accountsDB = await accountsDBPromise; const account = (await accountsDB.accounts .where("did") @@ -496,6 +518,7 @@ export const generateSaveAndActivateIdentity = async (): Promise => { const newId = newIdentifier(address, publicHex, privateHex, derivationPath); const identity = JSON.stringify(newId); + // one of the few times we use accountsDBPromise directly; try to avoid more usage const accountsDB = await accountsDBPromise; await accountsDB.accounts.add({ dateCreated: new Date().toISOString(), @@ -527,6 +550,7 @@ export const registerAndSavePasskey = async ( passkeyCredIdHex, publicKeyHex: Buffer.from(publicKeyBytes).toString("hex"), }; + // one of the few times we use accountsDBPromise directly; try to avoid more usage const accountsDB = await accountsDBPromise; await accountsDB.accounts.add(account); return account; diff --git a/src/router/index.ts b/src/router/index.ts index efd96bb..bffa5b4 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -18,6 +18,7 @@ const enterOrStart = async ( from: RouteLocationNormalized, next: NavigationGuardNext, ) => { + // one of the few times we use accountsDBPromise directly; try to avoid more usage const accountsDB = await accountsDBPromise; const num_accounts = await accountsDB.accounts.count(); if (num_accounts > 0) { @@ -263,6 +264,9 @@ const errorHandler = ( ) => { // Handle the error here console.error("Caught in top level error handler:", error, to, from); + alert( + "Something is very wrong. We'd love if you contacted us and let us know how you got here. Thank you!", + ); // You can also perform additional actions, such as displaying an error message or redirecting the user to a specific page }; diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index b992359..1298f97 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -951,8 +951,8 @@ export default class AccountViewView extends Vue { { group: "alert", type: "danger", - title: "Error Loading Account", - text: "Clear your cache and start over (after data backup).", + title: "Error Loading Profile", + text: "See the Help page about errors with your personal data.", }, -1, ); diff --git a/src/views/ClaimView.vue b/src/views/ClaimView.vue index ce7898a..1e593e3 100644 --- a/src/views/ClaimView.vue +++ b/src/views/ClaimView.vue @@ -484,7 +484,11 @@ import { useClipboard } from "@vueuse/core"; import GiftedDialog from "@/components/GiftedDialog.vue"; import QuickNav from "@/components/QuickNav.vue"; import { NotificationIface } from "@/constants/app"; -import { db, retrieveSettingsForActiveAccount } from "@/db/index"; +import { + db, + logConsoleAndDb, + retrieveSettingsForActiveAccount, +} from "@/db/index"; import { Contact } from "@/db/tables/contacts"; import * as serverUtil from "@/libs/endorserServer"; import { @@ -559,7 +563,24 @@ export default class ClaimView extends Vue { this.allContacts = await db.contacts.toArray(); this.isRegistered = settings.isRegistered || false; - this.allMyDids = await libsUtil.retrieveAccountDids(); + try { + this.allMyDids = await libsUtil.retrieveAccountDids(); + } catch (error) { + // continue because we want to see claims, even anonymously + logConsoleAndDb( + "Error retrieving all account DIDs on home page:" + error, + true, + ); + this.$notify( + { + group: "alert", + type: "danger", + title: "Error Loading Profile", + text: "See the Help page for problems with your personal data.", + }, + -1, + ); + } const pathParam = window.location.pathname.substring("/claim/".length); let claimId; diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index 408fa4d..5bb378d 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -171,8 +171,9 @@ - {{ shortDid(contact.did) }}...{{ + shortDid(contact.did) + }}
@@ -425,7 +426,7 @@ export default class ContactsView extends Vue { group: "alert", type: "warning", title: "Blank Invite", - text: "The invite was not included. This can happen when your device cuts off the link, so you might try pasting the full link into a browser.", + text: "The invite was not included, which can happen when your iOS device cuts off the link. Try pasting the full link into a browser.", }, 7000, ); @@ -601,7 +602,7 @@ export default class ContactsView extends Vue { }; try { - const headers = await getHeaders(this.activeDid); + const headers = await getHeaders(this.activeDid, this.$notify); const givenByUrl = this.apiServer + "/api/v2/report/gives?agentDid=" + diff --git a/src/views/HelpNotificationsView.vue b/src/views/HelpNotificationsView.vue index 63bde2b..e2c32c7 100644 --- a/src/views/HelpNotificationsView.vue +++ b/src/views/HelpNotificationsView.vue @@ -75,6 +75,7 @@ +

@@ -193,14 +194,18 @@

Reinstall

- If all else fails, uninstall the app, ensure all the browser tabs with - it are closed, and clear out caches and storage. + If all else fails, it's best to start over.

Of course, you'll want to back up all your data first -- all seeds as - well as the contacts & settings -- on the Account + well as the contacts & settings -- on the Profile page.

+

+ Here are instructions to uninstall the app and clear out caches and storage. + Note that you should first ensure check that the browser tabs with Time Safari are closed. + (If any are open then that will interfere with your refresh.) +

-
You do not have an active identifier.
+
You do not have an active identity.
@@ -135,7 +135,7 @@ export default class SeedBackupView extends Vue { { group: "alert", type: "danger", - title: "Error Loading Account", + title: "Error Loading Profile", text: "Got an error loading your seed data.", }, -1,