about summary refs log tree commit diff
path: root/src/state/persisted
diff options
context:
space:
mode:
Diffstat (limited to 'src/state/persisted')
-rw-r--r--src/state/persisted/broadcast/index.ts6
-rw-r--r--src/state/persisted/broadcast/index.web.ts1
-rw-r--r--src/state/persisted/index.ts15
-rw-r--r--src/state/persisted/legacy.ts84
-rw-r--r--src/state/persisted/schema.ts20
-rw-r--r--src/state/persisted/store.ts9
6 files changed, 94 insertions, 41 deletions
diff --git a/src/state/persisted/broadcast/index.ts b/src/state/persisted/broadcast/index.ts
deleted file mode 100644
index e0e7f724b..000000000
--- a/src/state/persisted/broadcast/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export default class BroadcastChannel {
-  constructor(public name: string) {}
-  postMessage(_data: any) {}
-  close() {}
-  onmessage: (event: MessageEvent) => void = () => {}
-}
diff --git a/src/state/persisted/broadcast/index.web.ts b/src/state/persisted/broadcast/index.web.ts
deleted file mode 100644
index 33b3548ad..000000000
--- a/src/state/persisted/broadcast/index.web.ts
+++ /dev/null
@@ -1 +0,0 @@
-export default BroadcastChannel
diff --git a/src/state/persisted/index.ts b/src/state/persisted/index.ts
index 67fac6b65..545fdc0e1 100644
--- a/src/state/persisted/index.ts
+++ b/src/state/persisted/index.ts
@@ -3,10 +3,10 @@ import {logger} from '#/logger'
 import {defaults, Schema} from '#/state/persisted/schema'
 import {migrate} from '#/state/persisted/legacy'
 import * as store from '#/state/persisted/store'
-import BroadcastChannel from '#/state/persisted/broadcast'
+import BroadcastChannel from '#/lib/broadcast'
 
-export type {Schema} from '#/state/persisted/schema'
-export {defaults as schema} from '#/state/persisted/schema'
+export type {Schema, PersistedAccount} from '#/state/persisted/schema'
+export {defaults} from '#/state/persisted/schema'
 
 const broadcast = new BroadcastChannel('BSKY_BROADCAST_CHANNEL')
 const UPDATE_EVENT = 'BSKY_UPDATE'
@@ -19,7 +19,7 @@ const _emitter = new EventEmitter()
  * the Provider.
  */
 export async function init() {
-  logger.debug('persisted state: initializing')
+  logger.info('persisted state: initializing')
 
   broadcast.onmessage = onBroadcastMessage
 
@@ -28,11 +28,12 @@ export async function init() {
     const stored = await store.read() // check for new store
     if (!stored) await store.write(defaults) // opt: init new store
     _state = stored || defaults // return new store
+    logger.log('persisted state: initialized')
   } catch (e) {
     logger.error('persisted state: failed to load root state from storage', {
       error: e,
     })
-    // AsyncStorage failured, but we can still continue in memory
+    // AsyncStorage failure, but we can still continue in memory
     return defaults
   }
 }
@@ -50,7 +51,9 @@ export async function write<K extends keyof Schema>(
     await store.write(_state)
     // must happen on next tick, otherwise the tab will read stale storage data
     setTimeout(() => broadcast.postMessage({event: UPDATE_EVENT}), 0)
-    logger.debug(`persisted state: wrote root state to storage`)
+    logger.debug(`persisted state: wrote root state to storage`, {
+      updatedKey: key,
+    })
   } catch (e) {
     logger.error(`persisted state: failed writing root state to storage`, {
       error: e,
diff --git a/src/state/persisted/legacy.ts b/src/state/persisted/legacy.ts
index 67eef81a0..025877529 100644
--- a/src/state/persisted/legacy.ts
+++ b/src/state/persisted/legacy.ts
@@ -66,45 +66,47 @@ type LegacySchema = {
 
 const DEPRECATED_ROOT_STATE_STORAGE_KEY = 'root'
 
-export function transform(legacy: LegacySchema): Schema {
+// TODO remove, assume that partial data may be here during our refactor
+export function transform(legacy: Partial<LegacySchema>): Schema {
   return {
     colorMode: legacy.shell?.colorMode || defaults.colorMode,
     session: {
-      accounts: legacy.session.accounts || defaults.session.accounts,
+      accounts: legacy.session?.accounts || defaults.session.accounts,
       currentAccount:
-        legacy.session.accounts.find(a => a.did === legacy.session.data.did) ||
-        defaults.session.currentAccount,
+        legacy.session?.accounts?.find(
+          a => a.did === legacy.session?.data?.did,
+        ) || defaults.session.currentAccount,
     },
     reminders: {
       lastEmailConfirm:
-        legacy.reminders.lastEmailConfirm ||
+        legacy.reminders?.lastEmailConfirm ||
         defaults.reminders.lastEmailConfirm,
     },
     languagePrefs: {
       primaryLanguage:
-        legacy.preferences.primaryLanguage ||
+        legacy.preferences?.primaryLanguage ||
         defaults.languagePrefs.primaryLanguage,
       contentLanguages:
-        legacy.preferences.contentLanguages ||
+        legacy.preferences?.contentLanguages ||
         defaults.languagePrefs.contentLanguages,
       postLanguage:
-        legacy.preferences.postLanguage || defaults.languagePrefs.postLanguage,
+        legacy.preferences?.postLanguage || defaults.languagePrefs.postLanguage,
       postLanguageHistory:
-        legacy.preferences.postLanguageHistory ||
+        legacy.preferences?.postLanguageHistory ||
         defaults.languagePrefs.postLanguageHistory,
+      appLanguage:
+        legacy.preferences?.postLanguage || defaults.languagePrefs.appLanguage,
     },
     requireAltTextEnabled:
-      legacy.preferences.requireAltTextEnabled ||
+      legacy.preferences?.requireAltTextEnabled ||
       defaults.requireAltTextEnabled,
-    mutedThreads: legacy.mutedThreads.uris || defaults.mutedThreads,
-    invitedUsers: {
-      seenDids: legacy.invitedUsers.seenDids || defaults.invitedUsers.seenDids,
+    mutedThreads: legacy.mutedThreads?.uris || defaults.mutedThreads,
+    invites: {
       copiedInvites:
-        legacy.invitedUsers.copiedInvites ||
-        defaults.invitedUsers.copiedInvites,
+        legacy.invitedUsers?.copiedInvites || defaults.invites.copiedInvites,
     },
     onboarding: {
-      step: legacy.onboarding.step || defaults.onboarding.step,
+      step: legacy.onboarding?.step || defaults.onboarding.step,
     },
   }
 }
@@ -114,20 +116,52 @@ export function transform(legacy: LegacySchema): Schema {
  * local storage AND old storage exists.
  */
 export async function migrate() {
-  logger.debug('persisted state: migrate')
+  logger.info('persisted state: migrate')
 
   try {
     const rawLegacyData = await AsyncStorage.getItem(
       DEPRECATED_ROOT_STATE_STORAGE_KEY,
     )
-    const alreadyMigrated = Boolean(await read())
+    const newData = await read()
+    const alreadyMigrated = Boolean(newData)
+
+    try {
+      if (rawLegacyData) {
+        const legacy = JSON.parse(rawLegacyData) as Partial<LegacySchema>
+        logger.info(`persisted state: debug legacy data`, {
+          hasExistingLoggedInAccount: Boolean(legacy?.session?.data),
+          numberOfExistingAccounts: legacy?.session?.accounts?.length,
+          foundExistingCurrentAccount: Boolean(
+            legacy.session?.accounts?.find(
+              a => a.did === legacy.session?.data?.did,
+            ),
+          ),
+        })
+        logger.info(`persisted state: debug new data`, {
+          hasExistingLoggedInAccount: Boolean(newData?.session?.currentAccount),
+          numberOfExistingAccounts: newData?.session?.accounts?.length,
+          existingAccountMatchesLegacy: Boolean(
+            newData?.session?.currentAccount?.did ===
+              legacy?.session?.data?.did,
+          ),
+        })
+      } else {
+        logger.info(`persisted state: no legacy to debug, fresh install`)
+      }
+    } catch (e) {
+      logger.error(`persisted state: legacy debugging failed`, {error: e})
+    }
 
     if (!alreadyMigrated && rawLegacyData) {
-      logger.debug('persisted state: migrating legacy storage')
+      logger.info('persisted state: migrating legacy storage')
       const legacyData = JSON.parse(rawLegacyData)
       const newData = transform(legacyData)
       await write(newData)
-      logger.debug('persisted state: migrated legacy storage')
+      // track successful migrations
+      logger.log('persisted state: migrated legacy storage')
+    } else {
+      // track successful migrations
+      logger.log('persisted state: no migration needed')
     }
   } catch (e) {
     logger.error('persisted state: error migrating legacy storage', {
@@ -135,3 +169,13 @@ export async function migrate() {
     })
   }
 }
+
+export async function clearLegacyStorage() {
+  try {
+    await AsyncStorage.removeItem(DEPRECATED_ROOT_STATE_STORAGE_KEY)
+  } catch (e: any) {
+    logger.error(`persisted legacy store: failed to clear`, {
+      error: e.toString(),
+    })
+  }
+}
diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts
index c00ee500a..71f9bd545 100644
--- a/src/state/persisted/schema.ts
+++ b/src/state/persisted/schema.ts
@@ -2,15 +2,19 @@ import {z} from 'zod'
 import {deviceLocales} from '#/platform/detection'
 
 // only data needed for rendering account page
+// TODO agent.resumeSession requires the following fields
 const accountSchema = z.object({
   service: z.string(),
   did: z.string(),
-  refreshJwt: z.string().optional(),
-  accessJwt: z.string().optional(),
   handle: z.string(),
-  displayName: z.string(),
-  aviUrl: z.string(),
+  email: z.string(),
+  emailConfirmed: z.boolean(),
+  refreshJwt: z.string().optional(), // optional because it can expire
+  accessJwt: z.string().optional(), // optional because it can expire
+  // displayName: z.string().optional(),
+  // aviUrl: z.string().optional(),
 })
+export type PersistedAccount = z.infer<typeof accountSchema>
 
 export const schema = z.object({
   colorMode: z.enum(['system', 'light', 'dark']),
@@ -26,11 +30,11 @@ export const schema = z.object({
     contentLanguages: z.array(z.string()), // should move to server
     postLanguage: z.string(), // should move to server
     postLanguageHistory: z.array(z.string()),
+    appLanguage: z.string(),
   }),
   requireAltTextEnabled: z.boolean(), // should move to server
   mutedThreads: z.array(z.string()), // should move to server
-  invitedUsers: z.object({
-    seenDids: z.array(z.string()),
+  invites: z.object({
     copiedInvites: z.array(z.string()),
   }),
   onboarding: z.object({
@@ -55,11 +59,11 @@ export const defaults: Schema = {
     postLanguageHistory: (deviceLocales || [])
       .concat(['en', 'ja', 'pt', 'de'])
       .slice(0, 6),
+    appLanguage: deviceLocales[0] || 'en',
   },
   requireAltTextEnabled: false,
   mutedThreads: [],
-  invitedUsers: {
-    seenDids: [],
+  invites: {
     copiedInvites: [],
   },
   onboarding: {
diff --git a/src/state/persisted/store.ts b/src/state/persisted/store.ts
index 2b03bec20..04858fe5b 100644
--- a/src/state/persisted/store.ts
+++ b/src/state/persisted/store.ts
@@ -1,6 +1,7 @@
 import AsyncStorage from '@react-native-async-storage/async-storage'
 
 import {Schema, schema} from '#/state/persisted/schema'
+import {logger} from '#/logger'
 
 const BSKY_STORAGE = 'BSKY_STORAGE'
 
@@ -16,3 +17,11 @@ export async function read(): Promise<Schema | undefined> {
     return objData
   }
 }
+
+export async function clear() {
+  try {
+    await AsyncStorage.removeItem(BSKY_STORAGE)
+  } catch (e: any) {
+    logger.error(`persisted store: failed to clear`, {error: e.toString()})
+  }
+}