From eab4ce561dfc6cdb4c54d56945e7051ec87ffd96 Mon Sep 17 00:00:00 2001
From: Trent Larson <trent@trentlarson.com>
Date: Tue, 10 Dec 2024 20:02:49 -0700
Subject: [PATCH] catch more errors if something catastrophic happens to
 encrypted data

---
 src/App.vue                                   | 22 +++++
 src/components/PushNotificationPermission.vue | 10 +--
 src/components/QuickNav.vue                   |  6 ++
 src/libs/endorserServer.ts                    | 82 ++++++++++++++-----
 src/libs/util.ts                              | 24 ++++++
 src/router/index.ts                           |  4 +
 src/views/AccountViewView.vue                 |  4 +-
 src/views/ClaimView.vue                       | 25 +++++-
 src/views/ContactsView.vue                    |  9 +-
 src/views/HelpNotificationsView.vue           | 37 ++++++---
 src/views/HelpView.vue                        | 18 ++--
 src/views/HomeView.vue                        | 33 ++++++--
 src/views/IdentitySwitcherView.vue            |  1 +
 src/views/ProjectViewView.vue                 | 25 +++++-
 src/views/ProjectsView.vue                    |  2 +-
 src/views/SeedBackupView.vue                  |  4 +-
 16 files changed, 237 insertions(+), 69 deletions(-)

diff --git a/src/App.vue b/src/App.vue
index 2ec923904..50249b2e6 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -309,6 +309,28 @@
               </div>
             </div>
           </div>
+
+          <div
+            v-else
+            class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
+          >
+            <div
+              class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
+            >
+              <div class="w-full px-6 py-6 text-slate-900 text-center">
+                <p class="text-lg mb-4">
+                  Something has gone very wrong. We'd appreciate if you'd
+                  contact us and let us know how you got here. Thank you!
+                </p>
+                <button
+                  @click="close(notification.id)"
+                  class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
+                >
+                  Close
+                </button>
+              </div>
+            </div>
+          </div>
         </div>
       </Notification>
     </div>
diff --git a/src/components/PushNotificationPermission.vue b/src/components/PushNotificationPermission.vue
index dd9d283d8..54af4f805 100644
--- a/src/components/PushNotificationPermission.vue
+++ b/src/components/PushNotificationPermission.vue
@@ -9,7 +9,7 @@
   >
     <div
       v-if="isVisible"
-      class="fixed z-[100] top-0 inset-x-0 w-full absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
+      class="fixed z-[100] top-0 inset-x-0 w-full inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
     >
       <div
         class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
@@ -276,9 +276,9 @@ export default class PushNotificationPermission extends Vue {
   }
 
   private async askPermission(): Promise<NotificationPermission> {
-    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 58b02c29f..ade216143 100644
--- a/src/components/QuickNav.vue
+++ b/src/components/QuickNav.vue
@@ -90,6 +90,12 @@
         >
           <div class="flex flex-col items-center">
             <fa icon="circle-user" class="fa-fw" />
+            <!--
+             We used to say "account", so we'll keep that in the code,
+             but it isn't accurate because we don't hold anything for them.
+             We'll say "profile" to the users.
+             (Or: settings, face, registry, cache, repo, vault... or separate preferences from identity.)
+            -->
             <span class="text-xs mt-1">profile</span>
           </div>
         </router-link>
diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts
index fedf43681..ed44c399a 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 f449cbee9..653e78447 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<number> => {
+  // 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<string[]> => {
+  // 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<string[]> => {
 export const retrieveAccountMetadata = async (
   activeDid: string,
 ): Promise<AccountKeyInfo | undefined> => {
+  // 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<Account[]> => {
+  // 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<Account[]> => {
 export const retrieveFullyDecryptedAccount = async (
   activeDid: string,
 ): Promise<AccountKeyInfo | undefined> => {
+  // 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<string> => {
   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 efd96bb5e..bffa5b4d2 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 b99235970..1298f9758 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 ce7898a91..1e593e32a 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 408fa4d53..5bb378daf 100644
--- a/src/views/ContactsView.vue
+++ b/src/views/ContactsView.vue
@@ -171,8 +171,9 @@
               <fa icon="circle-info" class="text-xl text-blue-500 ml-4" />
             </router-link>
 
-            <span class="ml-4 text-sm overflow-hidden"
-              >{{ shortDid(contact.did) }}...</span
+            <span class="ml-4 text-sm overflow-hidden">{{
+              shortDid(contact.did)
+            }}</span
             ><!-- The first 18 characters of did:peer are the same. -->
           </div>
           <div id="ContactActions" class="flex gap-1.5 mt-2">
@@ -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 63bde2b18..e2c32c7d3 100644
--- a/src/views/HelpNotificationsView.vue
+++ b/src/views/HelpNotificationsView.vue
@@ -75,6 +75,7 @@
           <button class="text-blue-500" @click="showNotificationChoice()">
             Click here.
           </button>
+          <PushNotificationPermission ref="pushNotificationPermission" />
         </p>
       </div>
 
@@ -193,14 +194,18 @@
       <h2 class="text-xl font-semibold mt-4">Reinstall</h2>
       <div>
         <p>
-          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.
         </p>
         <p>
           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
           <fa icon="circle-user" /> page.
         </p>
+        <p>
+          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.)
+        </p>
         <ul class="ml-4 list-disc">
           <li>
             Clear cache.
@@ -304,9 +309,12 @@ import { Component, Vue } from "vue-facing-decorator";
 
 import QuickNav from "@/components/QuickNav.vue";
 import { NotificationIface } from "@/constants/app";
-import { sendTestThroughPushServer } from "@/libs/util";
+import { DIRECT_PUSH_TITLE, sendTestThroughPushServer } from "@/libs/util";
+import PushNotificationPermission from "@/components/PushNotificationPermission.vue";
+import { db } from "@/db/index";
+import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
 
-@Component({ components: { QuickNav } })
+@Component({ components: { PushNotificationPermission, QuickNav } })
 export default class HelpNotificationsView extends Vue {
   $notify!: (notification: NotificationIface, timeout?: number) => void;
 
@@ -407,14 +415,19 @@ export default class HelpNotificationsView extends Vue {
   }
 
   showNotificationChoice() {
-    this.$notify(
-      {
-        group: "modal",
-        type: "notification-permission",
-        title: "", // unused, only here to satisfy type check
-        text: "", // unused, only here to satisfy type check
+    (this.$refs.pushNotificationPermission as PushNotificationPermission).open(
+      DIRECT_PUSH_TITLE,
+      async (success: boolean, timeText: string, message?: string) => {
+        if (success) {
+          await db.settings.update(MASTER_SETTINGS_KEY, {
+            notifyingReminderMessage: message,
+            notifyingReminderTime: timeText,
+          });
+          this.notifyingReminder = true;
+          this.notifyingReminderMessage = message || "";
+          this.notifyingReminderTime = timeText;
+        }
       },
-      -1,
     );
   }
 }
diff --git a/src/views/HelpView.vue b/src/views/HelpView.vue
index b9bf4f0d1..dbbe34409 100644
--- a/src/views/HelpView.vue
+++ b/src/views/HelpView.vue
@@ -383,7 +383,7 @@
         How do I access even more functionality?
       </h2>
       <p>
-        There is an "Advanced" section at the bottom of the Account
+        There is an "Advanced" section at the bottom of the Profile
         <fa icon="circle-user" /> page.
       </p>
       <p>
@@ -422,19 +422,19 @@
       </p>
 
       <h2 class="text-xl font-semibold">
-        My app is misbehaving, like showing me a blank screen or failing to show a feed.
+        This app is misbehaving, like showing me a blank screen or failing to show my personal data.
         What can I do?
       </h2>
       <p>
         First, note that clearing the cache will clear all your identity and contact info,
-        so we recommend doing other things first (unless you know you have your backups ready).
+        so we recommend doing other things first -- and only clearing when have your backups ready.
       </p>
       <ul class="list-disc list-outside ml-4">
         <li>
           Drag down on the screen to refresh it; do that multiple times, because
-          it sometimes takes multiple tries for the app to refresh to the current version.
+          it sometimes takes multiple tries for the app to refresh to the latest version.
           You can see the version information at the bottom of this page; the best
-          way to determine the current version is to open this page in an incognito
+          way to determine the latest version is to open this page in an incognito/private
           browser window and look at the version there.
         </li>
         <li>
@@ -498,7 +498,7 @@
         </a>
         <br />
         For notifications, this service stores push token data; that can be revoked at any time
-        by disabling notifications on the Account <fa icon="circle-user" class="fa-fw" /> page.
+        by disabling notifications on the Profile <fa icon="circle-user" class="fa-fw" /> page.
         <br />
         For all other claim data,
         <a href="https://endorser.ch/privacy-policy" target="_blank" class="text-blue-500">
@@ -520,9 +520,9 @@
           class="text-blue-500 ml-2"
         >
           bc1q90v4ted6cpt63tjfh2lvd5xzfc67sd4g9w8xma
-          <fa v-show="!showDidCopy" icon="copy" class="text-slate-400 fa-fw" />
+          <fa v-show="!showDidCopy" icon="copy" class="text-sm text-slate-400 fa-fw" />
+          <fa v-show="showDidCopy" icon="circle-check" class="text-sm text-green-500 fa-fw"/>
         </button>
-        <span v-show="showDidCopy" class="ml-2 text-sm text-green-500">Copied</span>
         You can donate online via
         <a href="https://www.patreon.com/TimeSafari" target="_blank" class="text-blue-500">Patreon here</a>.
         For other donations, contact us.
@@ -541,7 +541,7 @@
       <p>{{ package.version }} ({{ commitHash }})</p>
 
       <h2 class="text-xl font-semibold">
-        I have other questions or feedback, like getting a new account or removing my data or requesting an improvement.
+        I have other questions or feedback, like getting a new profile or removing my data or requesting an improvement.
       </h2>
       <p>
         Contact us at
diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue
index bb9bd1cd6..8e4de4442 100644
--- a/src/views/HomeView.vue
+++ b/src/views/HomeView.vue
@@ -389,6 +389,7 @@ import {
 } from "@/constants/app";
 import {
   db,
+  logConsoleAndDb,
   retrieveSettingsForActiveAccount,
   updateAccountSettings,
 } from "@/db/index";
@@ -486,12 +487,21 @@ export default class HomeView extends Vue {
 
   async mounted() {
     try {
-      this.allMyDids = await retrieveAccountDids();
-      if (this.allMyDids.length === 0) {
-        this.isCreatingIdentifier = true;
-        const newDid = await generateSaveAndActivateIdentity();
-        this.isCreatingIdentifier = false;
-        this.allMyDids = [newDid];
+      try {
+        this.allMyDids = await retrieveAccountDids();
+        if (this.allMyDids.length === 0) {
+          this.isCreatingIdentifier = true;
+          const newDid = await generateSaveAndActivateIdentity();
+          this.isCreatingIdentifier = false;
+          this.allMyDids = [newDid];
+        }
+      } catch (error) {
+        // continue because we want the feed to work, even anonymously
+        logConsoleAndDb(
+          "Error retrieving all account DIDs on home page:" + error,
+          true,
+        );
+        // some other piece will display an error about personal info
       }
 
       const settings = await retrieveSettingsForActiveAccount();
@@ -546,6 +556,7 @@ export default class HomeView extends Vue {
           this.activeDid,
           this.lastAckedOfferToUserJwtId,
         );
+        console.log("offersToUserData", offersToUserData);
         this.numNewOffersToUser = offersToUserData.data.length;
         this.newOffersToUserHitLimit = offersToUserData.hitLimit;
       }
@@ -563,7 +574,7 @@ export default class HomeView extends Vue {
 
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
     } catch (err: any) {
-      console.error("Error retrieving settings or feed.", err);
+      logConsoleAndDb("Error retrieving settings or feed: " + err, true);
       this.$notify(
         {
           group: "alert",
@@ -760,13 +771,19 @@ export default class HomeView extends Vue {
    */
   async retrieveGives(endorserApiServer: string, beforeId?: string) {
     const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
+    const doNotShowErrorAgain = !!beforeId; // don't show error again if we're loading more
+    const headers = await getHeaders(
+      this.activeDid,
+      doNotShowErrorAgain ? undefined : this.$notify,
+    );
+    // retrieve headers for this user, but if an error happens then report it but proceed with the fetch with no header
     const response = await fetch(
       endorserApiServer +
         "/api/v2/report/gives?giftNotTrade=true" +
         beforeQuery,
       {
         method: "GET",
-        headers: await getHeaders(this.activeDid),
+        headers: headers,
       },
     );
 
diff --git a/src/views/IdentitySwitcherView.vue b/src/views/IdentitySwitcherView.vue
index bfefd943f..43922034f 100644
--- a/src/views/IdentitySwitcherView.vue
+++ b/src/views/IdentitySwitcherView.vue
@@ -170,6 +170,7 @@ export default class IdentitySwitcherView extends Vue {
         title: "Delete Identity?",
         text: "Are you sure you want to erase this identity? (There is no undo. You may want to select it and back it up just in case.)",
         onYes: async () => {
+          // one of the few times we use accountsDBPromise directly; try to avoid more usage
           const accountsDB = await accountsDBPromise;
           await accountsDB.accounts.delete(id);
           this.otherIdentities = this.otherIdentities.filter(
diff --git a/src/views/ProjectViewView.vue b/src/views/ProjectViewView.vue
index 685e88f8f..787d7fbe8 100644
--- a/src/views/ProjectViewView.vue
+++ b/src/views/ProjectViewView.vue
@@ -489,7 +489,11 @@ import QuickNav from "@/components/QuickNav.vue";
 import EntityIcon from "@/components/EntityIcon.vue";
 import ProjectIcon from "@/components/ProjectIcon.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 libsUtil from "@/libs/util";
 import {
@@ -557,7 +561,24 @@ export default class ProjectViewView extends Vue {
     this.allContacts = await db.contacts.toArray();
     this.isRegistered = !!settings.isRegistered;
 
-    this.allMyDids = await retrieveAccountDids();
+    try {
+      this.allMyDids = await 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 to fix problems with your personal data.",
+        },
+        -1,
+      );
+    }
 
     const pathParam = window.location.pathname.substring("/project/".length);
     if (pathParam) {
diff --git a/src/views/ProjectsView.vue b/src/views/ProjectsView.vue
index 32fea4ea1..d1518130a 100644
--- a/src/views/ProjectsView.vue
+++ b/src/views/ProjectsView.vue
@@ -355,7 +355,7 @@ export default class ProjectsView extends Vue {
    **/
   async projectDataLoader(url: string) {
     try {
-      const headers = await getHeaders(this.activeDid);
+      const headers = await getHeaders(this.activeDid, this.$notify);
       this.isLoading = true;
       const resp = await this.axios.get(url, { headers } as AxiosRequestConfig);
       if (resp.status === 200 && resp.data.data) {
diff --git a/src/views/SeedBackupView.vue b/src/views/SeedBackupView.vue
index 9604b014f..a991a794e 100644
--- a/src/views/SeedBackupView.vue
+++ b/src/views/SeedBackupView.vue
@@ -94,7 +94,7 @@
         </button>
       </div>
     </div>
-    <div v-else>You do not have an active identifier.</div>
+    <div v-else>You do not have an active identity.</div>
   </section>
 </template>
 
@@ -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,