Improvements to persisted state migration (#2098)

* Fix session email/emailConfirmed types, update usage for safer access

* Handle fallback better, better errors

* Better handling, add test

* Add test for default data

* Remove fallback, not needed, update logs
zio/stable
Eric Bailey 2023-12-05 19:59:34 -06:00 committed by GitHub
parent a915a57b10
commit 3c8036587e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 48 additions and 30 deletions

View File

@ -0,0 +1,13 @@
import {expect, test} from '@jest/globals'
import {transform} from '#/state/persisted/legacy'
import {defaults, schema} from '#/state/persisted/schema'
test('defaults', () => {
expect(() => schema.parse(defaults)).not.toThrow()
})
test('transform', () => {
const data = transform({})
expect(() => schema.parse(data)).not.toThrow()
})

View File

@ -26,7 +26,10 @@ export async function init() {
try {
await migrate() // migrate old store
const stored = await store.read() // check for new store
if (!stored) await store.write(defaults) // opt: init new store
if (!stored) {
logger.info('persisted state: initializing default storage')
await store.write(defaults) // opt: init new store
}
_state = stored || defaults // return new store
logger.log('persisted state: initialized')
} catch (e) {

View File

@ -1,7 +1,7 @@
import AsyncStorage from '@react-native-async-storage/async-storage'
import {logger} from '#/logger'
import {defaults, Schema} from '#/state/persisted/schema'
import {defaults, Schema, schema} from '#/state/persisted/schema'
import {write, read} from '#/state/persisted/store'
/**
@ -66,7 +66,6 @@ type LegacySchema = {
const DEPRECATED_ROOT_STATE_STORAGE_KEY = 'root'
// 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,
@ -116,7 +115,7 @@ export function transform(legacy: Partial<LegacySchema>): Schema {
* local storage AND old storage exists.
*/
export async function migrate() {
logger.info('persisted state: migrate')
logger.info('persisted state: check need to migrate')
try {
const rawLegacyData = await AsyncStorage.getItem(
@ -138,6 +137,7 @@ export async function migrate() {
),
})
logger.info(`persisted state: debug new data`, {
hasNewData: Boolean(newData),
hasExistingLoggedInAccount: Boolean(newData?.session?.currentAccount),
numberOfExistingAccounts: newData?.session?.accounts?.length,
existingAccountMatchesLegacy: Boolean(
@ -145,27 +145,32 @@ export async function migrate() {
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})
} catch (e: any) {
logger.error(e, {message: `persisted state: legacy debugging failed`})
}
if (!alreadyMigrated && rawLegacyData) {
logger.info('persisted state: migrating legacy storage')
const legacyData = JSON.parse(rawLegacyData)
const newData = transform(legacyData)
await write(newData)
// track successful migrations
logger.log('persisted state: migrated legacy storage')
const validate = schema.safeParse(newData)
if (validate.success) {
await write(newData)
logger.log('persisted state: migrated legacy storage')
} else {
logger.error('persisted state: legacy data failed validation', {
error: validate.error,
})
}
} else {
// track successful migrations
logger.log('persisted state: no migration needed')
}
} catch (e) {
logger.error('persisted state: error migrating legacy storage', {
error: String(e),
} catch (e: any) {
logger.error(e, {
message: 'persisted state: error migrating legacy storage',
})
}
}

View File

@ -2,17 +2,14 @@ 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(),
handle: z.string(),
email: z.string(),
emailConfirmed: z.boolean(),
email: z.string().optional(),
emailConfirmed: z.boolean().optional(),
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>

View File

@ -245,7 +245,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
service: agent.service.toString(),
did: agent.session.did,
handle: agent.session.handle,
email: agent.session.email!, // TODO this is always defined?
email: agent.session.email,
emailConfirmed: agent.session.emailConfirmed || false,
refreshJwt: agent.session.refreshJwt,
accessJwt: agent.session.accessJwt,
@ -342,7 +342,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
service: agent.service.toString(),
did: agent.session.did,
handle: agent.session.handle,
email: agent.session.email!, // TODO this is always defined?
email: agent.session.email,
emailConfirmed: agent.session.emailConfirmed || false,
refreshJwt: agent.session.refreshJwt,
accessJwt: agent.session.accessJwt,

View File

@ -118,8 +118,8 @@ export function Component() {
) : stage === Stages.ConfirmCode ? (
<Trans>
An email has been sent to your previous address,{' '}
{currentAccount?.email || ''}. It includes a confirmation code
which you can enter below.
{currentAccount?.email || '(no email)'}. It includes a
confirmation code which you can enter below.
</Trans>
) : (
<Trans>

View File

@ -108,8 +108,8 @@ export function Component({showReminder}: {showReminder?: boolean}) {
</Trans>
) : stage === Stages.ConfirmCode ? (
<Trans>
An email has been sent to {currentAccount?.email || ''}. It
includes a confirmation code which you can enter below.
An email has been sent to {currentAccount?.email || '(no email)'}.
It includes a confirmation code which you can enter below.
</Trans>
) : (
''
@ -125,7 +125,7 @@ export function Component({showReminder}: {showReminder?: boolean}) {
size={16}
/>
<Text type="xl-medium" style={[pal.text, s.flex1, {minWidth: 0}]}>
{currentAccount?.email || ''}
{currentAccount?.email || '(no email)'}
</Text>
</View>
<Pressable

View File

@ -299,7 +299,7 @@ export function SettingsScreen({}: Props) {
</>
)}
<Text type="lg" style={pal.text}>
{currentAccount.email}{' '}
{currentAccount.email || '(no email)'}{' '}
</Text>
<Link onPress={() => openModal({name: 'change-email'})}>
<Text type="lg" style={pal.link}>

View File

@ -58,8 +58,8 @@ export function DesktopRightNav() {
type="md"
style={pal.link}
href={FEEDBACK_FORM_URL({
email: currentAccount!.email,
handle: currentAccount!.handle,
email: currentAccount?.email,
handle: currentAccount?.handle,
})}
text={_(msg`Feedback`)}
/>