mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at verify-code 178 lines 5.6 kB view raw
1import {z} from 'zod' 2 3import {logger} from '#/logger' 4import {deviceLocales} from '#/platform/detection' 5import {PlatformInfo} from '../../../modules/expo-bluesky-swiss-army' 6 7const externalEmbedOptions = ['show', 'hide'] as const 8 9/** 10 * A account persisted to storage. Stored in the `accounts[]` array. Contains 11 * base account info and access tokens. 12 */ 13const accountSchema = z.object({ 14 service: z.string(), 15 did: z.string(), 16 handle: z.string(), 17 email: z.string().optional(), 18 emailConfirmed: z.boolean().optional(), 19 emailAuthFactor: z.boolean().optional(), 20 refreshJwt: z.string().optional(), // optional because it can expire 21 accessJwt: z.string().optional(), // optional because it can expire 22 signupQueued: z.boolean().optional(), 23 active: z.boolean().optional(), // optional for backwards compat 24 /** 25 * Known values: takendown, suspended, deactivated 26 * @see https://github.com/bluesky-social/atproto/blob/5441fbde9ed3b22463e91481ec80cb095643e141/lexicons/com/atproto/server/getSession.json 27 */ 28 status: z.string().optional(), 29 pdsUrl: z.string().optional(), 30}) 31export type PersistedAccount = z.infer<typeof accountSchema> 32 33/** 34 * The current account. Stored in the `currentAccount` field. 35 * 36 * In previous versions, this included tokens and other info. Now, it's used 37 * only to reference the `did` field, and all other fields are marked as 38 * optional. They should be considered deprecated and not used, but are kept 39 * here for backwards compat. 40 */ 41const currentAccountSchema = accountSchema.extend({ 42 service: z.string().optional(), 43 handle: z.string().optional(), 44}) 45export type PersistedCurrentAccount = z.infer<typeof currentAccountSchema> 46 47const schema = z.object({ 48 colorMode: z.enum(['system', 'light', 'dark']), 49 darkTheme: z.enum(['dim', 'dark']).optional(), 50 session: z.object({ 51 accounts: z.array(accountSchema), 52 currentAccount: currentAccountSchema.optional(), 53 }), 54 reminders: z.object({ 55 lastEmailConfirm: z.string().optional(), 56 }), 57 languagePrefs: z.object({ 58 primaryLanguage: z.string(), // should move to server 59 contentLanguages: z.array(z.string()), // should move to server 60 postLanguage: z.string(), // should move to server 61 postLanguageHistory: z.array(z.string()), 62 appLanguage: z.string(), 63 }), 64 requireAltTextEnabled: z.boolean(), // should move to server 65 largeAltBadgeEnabled: z.boolean().optional(), 66 externalEmbeds: z 67 .object({ 68 giphy: z.enum(externalEmbedOptions).optional(), 69 tenor: z.enum(externalEmbedOptions).optional(), 70 youtube: z.enum(externalEmbedOptions).optional(), 71 youtubeShorts: z.enum(externalEmbedOptions).optional(), 72 twitch: z.enum(externalEmbedOptions).optional(), 73 vimeo: z.enum(externalEmbedOptions).optional(), 74 spotify: z.enum(externalEmbedOptions).optional(), 75 appleMusic: z.enum(externalEmbedOptions).optional(), 76 soundcloud: z.enum(externalEmbedOptions).optional(), 77 flickr: z.enum(externalEmbedOptions).optional(), 78 }) 79 .optional(), 80 invites: z.object({ 81 copiedInvites: z.array(z.string()), 82 }), 83 onboarding: z.object({ 84 step: z.string(), 85 }), 86 hiddenPosts: z.array(z.string()).optional(), // should move to server 87 useInAppBrowser: z.boolean().optional(), 88 lastSelectedHomeFeed: z.string().optional(), 89 pdsAddressHistory: z.array(z.string()).optional(), 90 disableHaptics: z.boolean().optional(), 91 disableAutoplay: z.boolean().optional(), 92 kawaii: z.boolean().optional(), 93 hasCheckedForStarterPack: z.boolean().optional(), 94 subtitlesEnabled: z.boolean().optional(), 95 /** @deprecated */ 96 mutedThreads: z.array(z.string()), 97}) 98export type Schema = z.infer<typeof schema> 99 100export const defaults: Schema = { 101 colorMode: 'system', 102 darkTheme: 'dim', 103 session: { 104 accounts: [], 105 currentAccount: undefined, 106 }, 107 reminders: { 108 lastEmailConfirm: undefined, 109 }, 110 languagePrefs: { 111 primaryLanguage: deviceLocales[0] || 'en', 112 contentLanguages: deviceLocales || [], 113 postLanguage: deviceLocales[0] || 'en', 114 postLanguageHistory: (deviceLocales || []) 115 .concat(['en', 'ja', 'pt', 'de']) 116 .slice(0, 6), 117 appLanguage: deviceLocales[0] || 'en', 118 }, 119 requireAltTextEnabled: false, 120 largeAltBadgeEnabled: false, 121 externalEmbeds: {}, 122 mutedThreads: [], 123 invites: { 124 copiedInvites: [], 125 }, 126 onboarding: { 127 step: 'Home', 128 }, 129 hiddenPosts: [], 130 useInAppBrowser: undefined, 131 lastSelectedHomeFeed: undefined, 132 pdsAddressHistory: [], 133 disableHaptics: false, 134 disableAutoplay: PlatformInfo.getIsReducedMotionEnabled(), 135 kawaii: false, 136 hasCheckedForStarterPack: false, 137 subtitlesEnabled: true, 138} 139 140export function tryParse(rawData: string): Schema | undefined { 141 let objData 142 try { 143 objData = JSON.parse(rawData) 144 } catch (e) { 145 logger.error('persisted state: failed to parse root state from storage', { 146 message: e, 147 }) 148 } 149 if (!objData) { 150 return undefined 151 } 152 const parsed = schema.safeParse(objData) 153 if (parsed.success) { 154 return objData 155 } else { 156 const errors = 157 parsed.error?.errors?.map(e => ({ 158 code: e.code, 159 // @ts-ignore exists on some types 160 expected: e?.expected, 161 path: e.path?.join('.'), 162 })) || [] 163 logger.error(`persisted store: data failed validation on read`, {errors}) 164 return undefined 165 } 166} 167 168export function tryStringify(value: Schema): string | undefined { 169 try { 170 schema.parse(value) 171 return JSON.stringify(value) 172 } catch (e) { 173 logger.error(`persisted state: failed stringifying root state`, { 174 message: e, 175 }) 176 return undefined 177 } 178}