diff --git a/keyserver/src/responders/website-responders.js b/keyserver/src/responders/website-responders.js index d114cec78..c90cc7c76 100644 --- a/keyserver/src/responders/website-responders.js +++ b/keyserver/src/responders/website-responders.js @@ -1,822 +1,823 @@ // @flow import html from 'common-tags/lib/html/index.js'; import { detect as detectBrowser } from 'detect-browser'; import type { $Response, $Request } from 'express'; import fs from 'fs'; import _isEqual from 'lodash/fp/isEqual.js'; import _keyBy from 'lodash/fp/keyBy.js'; import * as React from 'react'; // eslint-disable-next-line import/extensions import ReactDOMServer from 'react-dom/server'; import t from 'tcomb'; import { promisify } from 'util'; import { inviteLinkUrl } from 'lib/facts/links.js'; import { baseLegalPolicies } from 'lib/facts/policies.js'; import stores from 'lib/facts/stores.js'; import { daysToEntriesFromEntryInfos } from 'lib/reducers/entry-reducer.js'; import { freshMessageStore } from 'lib/reducers/message-reducer.js'; import { mostRecentlyReadThread } from 'lib/selectors/thread-selectors.js'; import { mostRecentMessageTimestamp } from 'lib/shared/message-utils.js'; import { threadHasPermission, threadIsPending, parsePendingThreadID, createPendingThread, } from 'lib/shared/thread-utils.js'; import { defaultWebEnabledApps } from 'lib/types/enabled-apps.js'; import { entryStoreValidator } from 'lib/types/entry-types.js'; import { defaultCalendarFilters } from 'lib/types/filter-types.js'; import { inviteLinksStoreValidator } from 'lib/types/link-types.js'; import { defaultNumberPerThread, messageStoreValidator, } from 'lib/types/message-types.js'; import { defaultEnabledReports } from 'lib/types/report-types.js'; import { defaultConnectionInfo } from 'lib/types/socket-types.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; import { threadTypes } from 'lib/types/thread-types-enum.js'; import { threadStoreValidator } from 'lib/types/thread-types.js'; import { currentUserInfoValidator, userInfosValidator, } from 'lib/types/user-types.js'; import { currentDateInTimeZone } from 'lib/utils/date-utils.js'; import { ServerError } from 'lib/utils/errors.js'; import { promiseAll } from 'lib/utils/promises.js'; import { defaultNotifPermissionAlertInfo } from 'lib/utils/push-alerts.js'; import { infoFromURL } from 'lib/utils/url-utils.js'; import { tBool, tNumber, tShape, tString } from 'lib/utils/validation-utils.js'; import getTitle from 'web/title/getTitle.js'; import { navInfoValidator } from 'web/types/nav-types.js'; import { navInfoFromURL } from 'web/url-utils.js'; import { fetchEntryInfos } from '../fetchers/entry-fetchers.js'; import { fetchPrimaryInviteLinks } from '../fetchers/link-fetchers.js'; import { fetchMessageInfos } from '../fetchers/message-fetchers.js'; import { hasAnyNotAcknowledgedPolicies } from '../fetchers/policy-acknowledgment-fetchers.js'; import { fetchThreadInfos } from '../fetchers/thread-fetchers.js'; import { fetchCurrentUserInfo, fetchKnownUserInfos, fetchUserInfos, } from '../fetchers/user-fetchers.js'; import { getWebPushConfig } from '../push/providers.js'; import { setNewSession } from '../session/cookies.js'; import { Viewer } from '../session/viewer.js'; import { streamJSON, waitForStream } from '../utils/json-stream.js'; import { getAppURLFactsFromRequestURL, getCommAppURLFacts, } from '../utils/urls.js'; import { validateOutput } from '../utils/validation-utils.js'; const { renderToNodeStream } = ReactDOMServer; const access = promisify(fs.access); const readFile = promisify(fs.readFile); const googleFontsURL = 'https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=Inter:wght@400;500;600&display=swap'; const localFontsURL = 'fonts/local-fonts.css'; async function getFontsURL() { try { await access(localFontsURL); return localFontsURL; } catch { return googleFontsURL; } } type AssetInfo = { +jsURL: string, +fontsURL: string, +cssInclude: string, +olmFilename: string, +sqljsFilename: string, +commQueryExecutorFilename: string, +opaqueURL: string, }; let assetInfo: ?AssetInfo = null; async function getAssetInfo() { if (assetInfo) { return assetInfo; } if (process.env.NODE_ENV === 'development') { const fontsURL = await getFontsURL(); assetInfo = { jsURL: 'http://localhost:8080/dev.build.js', fontsURL, cssInclude: '', olmFilename: '', sqljsFilename: '', commQueryExecutorFilename: '', opaqueURL: 'http://localhost:8080/opaque-ke.wasm', }; return assetInfo; } try { const manifestString = await readFile('../web/dist/manifest.json', 'utf8'); const manifest = JSON.parse(manifestString); const webworkersManifestString = await readFile( '../web/dist/webworkers/manifest.json', 'utf8', ); const webworkersManifest = JSON.parse(webworkersManifestString); assetInfo = { jsURL: `compiled/${manifest['browser.js']}`, fontsURL: googleFontsURL, cssInclude: html` `, olmFilename: manifest['olm.wasm'], sqljsFilename: webworkersManifest['sql-wasm.wasm'], commQueryExecutorFilename: webworkersManifest['comm_query_executor.wasm'], opaqueURL: `compiled/${manifest['comm_opaque2_wasm_bg.wasm']}`, }; return assetInfo; } catch { throw new Error( 'Could not load manifest.json for web build. ' + 'Did you forget to run `yarn dev` in the web folder?', ); } } let webpackCompiledRootComponent: ?React.ComponentType<{}> = null; async function getWebpackCompiledRootComponentForSSR() { if (webpackCompiledRootComponent) { return webpackCompiledRootComponent; } try { // $FlowFixMe web/dist doesn't always exist const webpackBuild = await import('web/dist/app.build.cjs'); webpackCompiledRootComponent = webpackBuild.app.default; return webpackCompiledRootComponent; } catch { throw new Error( 'Could not load app.build.cjs. ' + 'Did you forget to run `yarn dev` in the web folder?', ); } } const initialReduxStateValidator = tShape({ navInfo: navInfoValidator, deviceID: t.Nil, currentUserInfo: currentUserInfoValidator, draftStore: t.irreducible('default draftStore', _isEqual({ drafts: {} })), sessionID: t.maybe(t.String), entryStore: entryStoreValidator, threadStore: threadStoreValidator, userStore: tShape({ userInfos: userInfosValidator, inconsistencyReports: t.irreducible( 'default inconsistencyReports', _isEqual([]), ), }), messageStore: messageStoreValidator, updatesCurrentAsOf: t.Number, loadingStatuses: t.irreducible('default loadingStatuses', _isEqual({})), calendarFilters: t.irreducible( 'defaultCalendarFilters', _isEqual(defaultCalendarFilters), ), communityPickerStore: t.irreducible( 'default communityPickerStore', _isEqual({ chat: null, calendar: null }), ), urlPrefix: tString(''), windowDimensions: t.irreducible( 'default windowDimensions', _isEqual({ width: 0, height: 0 }), ), baseHref: t.String, notifPermissionAlertInfo: t.irreducible( 'default notifPermissionAlertInfo', _isEqual(defaultNotifPermissionAlertInfo), ), connection: tShape({ status: tString('connecting'), queuedActivityUpdates: t.irreducible( 'default queuedActivityUpdates', _isEqual([]), ), actualizedCalendarQuery: tShape({ startDate: t.String, endDate: t.String, filters: t.irreducible( 'default filters', _isEqual(defaultCalendarFilters), ), }), lateResponses: t.irreducible('default lateResponses', _isEqual([])), showDisconnectedBar: tBool(false), }), watchedThreadIDs: t.irreducible('default watchedThreadIDs', _isEqual([])), lifecycleState: tString('active'), enabledApps: t.irreducible( 'defaultWebEnabledApps', _isEqual(defaultWebEnabledApps), ), reportStore: t.irreducible( 'default reportStore', _isEqual({ enabledReports: defaultEnabledReports, queuedReports: [], }), ), nextLocalID: tNumber(0), cookie: t.Nil, deviceToken: t.Nil, dataLoaded: t.Boolean, windowActive: tBool(true), userPolicies: t.irreducible('default userPolicies', _isEqual({})), cryptoStore: t.irreducible( 'default cryptoStore', _isEqual({ primaryIdentityKeys: null, notificationIdentityKeys: null, }), ), pushApiPublicKey: t.maybe(t.String), _persist: t.Nil, commServicesAccessToken: t.Nil, inviteLinksStore: inviteLinksStoreValidator, lastCommunicatedPlatformDetails: t.irreducible( 'default lastCommunicatedPlatformDetails', _isEqual({}), ), }); async function websiteResponder( viewer: Viewer, req: $Request, res: $Response, ): Promise { const appURLFacts = getAppURLFactsFromRequestURL(req.originalUrl); const { basePath, baseDomain } = appURLFacts; const baseURL = basePath.replace(/\/$/, ''); const baseHref = baseDomain + baseURL; const loadingPromise = getWebpackCompiledRootComponentForSSR(); const hasNotAcknowledgedPoliciesPromise = hasAnyNotAcknowledgedPolicies( viewer.id, baseLegalPolicies, ); const initialNavInfoPromise = (async () => { try { const urlInfo = infoFromURL(req.url); let backupInfo = { now: currentDateInTimeZone(viewer.timeZone), }; // Some user ids in selectedUserList might not exist in the userStore // (e.g. they were included in the results of the user search endpoint) // Because of that we keep their userInfos inside the navInfo. if (urlInfo.selectedUserList) { const fetchedUserInfos = await fetchUserInfos(urlInfo.selectedUserList); const userInfos = {}; for (const userID in fetchedUserInfos) { const userInfo = fetchedUserInfos[userID]; if (userInfo.username) { userInfos[userID] = userInfo; } } backupInfo = { userInfos, ...backupInfo }; } return navInfoFromURL(urlInfo, backupInfo); } catch (e) { throw new ServerError(e.message); } })(); const calendarQueryPromise = (async () => { const initialNavInfo = await initialNavInfoPromise; return { startDate: initialNavInfo.startDate, endDate: initialNavInfo.endDate, filters: defaultCalendarFilters, }; })(); const messageSelectionCriteria = { joinedThreads: true }; const initialTime = Date.now(); const assetInfoPromise = getAssetInfo(); const threadInfoPromise = fetchThreadInfos(viewer); const messageInfoPromise = fetchMessageInfos( viewer, messageSelectionCriteria, defaultNumberPerThread, ); const entryInfoPromise = (async () => { const calendarQuery = await calendarQueryPromise; return await fetchEntryInfos(viewer, [calendarQuery]); })(); const currentUserInfoPromise = fetchCurrentUserInfo(viewer); const userInfoPromise = fetchKnownUserInfos(viewer); const sessionIDPromise = (async () => { const calendarQuery = await calendarQueryPromise; if (viewer.loggedIn) { await setNewSession(viewer, calendarQuery, initialTime); } return viewer.sessionID; })(); const threadStorePromise = (async () => { const [{ threadInfos }, hasNotAcknowledgedPolicies] = await Promise.all([ threadInfoPromise, hasNotAcknowledgedPoliciesPromise, ]); return { threadInfos: hasNotAcknowledgedPolicies ? {} : threadInfos }; })(); const messageStorePromise = (async () => { const [ { threadInfos }, { rawMessageInfos, truncationStatuses }, hasNotAcknowledgedPolicies, ] = await Promise.all([ threadInfoPromise, messageInfoPromise, hasNotAcknowledgedPoliciesPromise, ]); if (hasNotAcknowledgedPolicies) { return { messages: {}, threads: {}, local: {}, currentAsOf: 0, }; } const { messageStore: freshStore } = freshMessageStore( rawMessageInfos, truncationStatuses, mostRecentMessageTimestamp(rawMessageInfos, initialTime), threadInfos, ); return freshStore; })(); const entryStorePromise = (async () => { const [{ rawEntryInfos }, hasNotAcknowledgedPolicies] = await Promise.all([ entryInfoPromise, hasNotAcknowledgedPoliciesPromise, ]); if (hasNotAcknowledgedPolicies) { return { entryInfos: {}, daysToEntries: {}, lastUserInteractionCalendar: 0, }; } return { entryInfos: _keyBy('id')(rawEntryInfos), daysToEntries: daysToEntriesFromEntryInfos(rawEntryInfos), lastUserInteractionCalendar: initialTime, }; })(); const userStorePromise = (async () => { const [userInfos, hasNotAcknowledgedPolicies] = await Promise.all([ userInfoPromise, hasNotAcknowledgedPoliciesPromise, ]); return { userInfos: hasNotAcknowledgedPolicies ? {} : userInfos, inconsistencyReports: [], }; })(); const navInfoPromise = (async () => { const [ { threadInfos }, messageStore, currentUserInfo, userStore, finalNavInfo, ] = await Promise.all([ threadInfoPromise, messageStorePromise, currentUserInfoPromise, userStorePromise, initialNavInfoPromise, ]); const requestedActiveChatThreadID = finalNavInfo.activeChatThreadID; if ( requestedActiveChatThreadID && !threadIsPending(requestedActiveChatThreadID) && !threadHasPermission( threadInfos[requestedActiveChatThreadID], threadPermissions.VISIBLE, ) ) { finalNavInfo.activeChatThreadID = null; } if (!finalNavInfo.activeChatThreadID) { const mostRecentThread = mostRecentlyReadThread( messageStore, threadInfos, ); if (mostRecentThread) { finalNavInfo.activeChatThreadID = mostRecentThread; } } if ( finalNavInfo.activeChatThreadID && threadIsPending(finalNavInfo.activeChatThreadID) && finalNavInfo.pendingThread?.id !== finalNavInfo.activeChatThreadID ) { const pendingThreadData = parsePendingThreadID( finalNavInfo.activeChatThreadID, ); if ( pendingThreadData && pendingThreadData.threadType !== threadTypes.SIDEBAR && currentUserInfo.id ) { const { userInfos } = userStore; const members = [...pendingThreadData.memberIDs, currentUserInfo.id] .map(id => { const userInfo = userInfos[id]; if (!userInfo || !userInfo.username) { return undefined; } const { username } = userInfo; return { id, username }; }) .filter(Boolean); const newPendingThread = createPendingThread({ viewerID: currentUserInfo.id, threadType: pendingThreadData.threadType, members, }); finalNavInfo.activeChatThreadID = newPendingThread.id; finalNavInfo.pendingThread = newPendingThread; } } return finalNavInfo; })(); const currentAsOfPromise = (async () => { const hasNotAcknowledgedPolicies = await hasNotAcknowledgedPoliciesPromise; return hasNotAcknowledgedPolicies ? 0 : initialTime; })(); const pushApiPublicKeyPromise = (async () => { const pushConfig = await getWebPushConfig(); if (!pushConfig) { if (process.env.NODE_ENV !== 'development') { console.warn('keyserver/secrets/web_push_config.json should exist'); } return null; } return pushConfig.publicKey; })(); const inviteLinksStorePromise = (async () => { const primaryInviteLinks = await fetchPrimaryInviteLinks(viewer); const links = {}; for (const link of primaryInviteLinks) { if (link.primary) { links[link.communityID] = { primaryLink: link, }; } } return { links, }; })(); const { jsURL, fontsURL, cssInclude, olmFilename, sqljsFilename, opaqueURL, commQueryExecutorFilename, } = await assetInfoPromise; // prettier-ignore res.write(html` ${getTitle(0)} ${cssInclude}
`); const Loading = await loadingPromise; const reactStream = renderToNodeStream(); reactStream.pipe(res, { end: false }); await waitForStream(reactStream); res.write(html`
`); } const inviteSecretRegex = /^[a-z0-9]+$/i; // On native, if this responder is called, it means that the app isn't // installed. async function inviteResponder(req: $Request, res: $Response): Promise { const { secret } = req.params; const userAgent = req.get('User-Agent'); const detectionResult = detectBrowser(userAgent); if (detectionResult.os === 'Android OS') { const isSecretValid = inviteSecretRegex.test(secret); const referrer = isSecretValid ? `&referrer=${encodeURIComponent(`utm_source=invite/${secret}`)}` : ''; const redirectUrl = `${stores.googlePlayUrl}${referrer}`; res.writeHead(301, { Location: redirectUrl, }); res.end(); return; } else if (detectionResult.os !== 'iOS') { const urlFacts = getCommAppURLFacts(); const baseDomain = urlFacts?.baseDomain ?? ''; const basePath = urlFacts?.basePath ?? '/'; const redirectUrl = `${baseDomain}${basePath}handle/invite/${secret}`; res.writeHead(301, { Location: redirectUrl, }); res.end(); return; } const fontsURL = await getFontsURL(); res.end(html` Comm

Comm

To join this community, download the Comm app and reopen this invite link

Download Comm Invite Link
Visit Comm’s website arrow up right `); } export { websiteResponder, inviteResponder }; diff --git a/keyserver/src/utils/validation-utils.js b/keyserver/src/utils/validation-utils.js index 0578109a5..37d638465 100644 --- a/keyserver/src/utils/validation-utils.js +++ b/keyserver/src/utils/validation-utils.js @@ -1,234 +1,238 @@ // @flow import type { TType } from 'tcomb'; import type { PolicyType } from 'lib/facts/policies.js'; import { hasMinCodeVersion, FUTURE_CODE_VERSION, } from 'lib/shared/version-utils.js'; -import { type PlatformDetails, isWebPlatform } from 'lib/types/device-types.js'; +import { type PlatformDetails } from 'lib/types/device-types.js'; import { ServerError } from 'lib/utils/errors.js'; import { tCookie, tPassword, tPlatform, tPlatformDetails, assertWithValidator, convertToNewIDSchema, keyserverPrefixID, convertClientIDsToServerIDs, convertObject, convertServerIDsToClientIDs, } from 'lib/utils/validation-utils.js'; import { fetchNotAcknowledgedPolicies } from '../fetchers/policy-acknowledgment-fetchers.js'; import { verifyClientSupported } from '../session/version.js'; import type { Viewer } from '../session/viewer.js'; async function validateInput( viewer: Viewer, inputValidator: TType, input: mixed, ): Promise { if (!viewer.isSocket) { await checkClientSupported(viewer, inputValidator, input); } const convertedInput = checkInputValidator(inputValidator, input); if ( hasMinCodeVersion(viewer.platformDetails, { native: FUTURE_CODE_VERSION, + web: FUTURE_CODE_VERSION, }) && - !isWebPlatform(viewer.platformDetails?.platform) && convertToNewIDSchema ) { try { return convertClientIDsToServerIDs( keyserverPrefixID, inputValidator, convertedInput, ); } catch (err) { throw new ServerError(err.message); } } return convertedInput; } function validateOutput( platformDetails: ?PlatformDetails, outputValidator: TType, data: T, + alwaysConvertSchema?: boolean, ): T { if (!outputValidator.is(data)) { console.trace( 'Output validation failed, validator is:', outputValidator.displayName, ); return data; } if ( - hasMinCodeVersion(platformDetails, { native: FUTURE_CODE_VERSION }) && - !isWebPlatform(platformDetails?.platform) && + (hasMinCodeVersion(platformDetails, { + native: FUTURE_CODE_VERSION, + web: FUTURE_CODE_VERSION, + }) || + alwaysConvertSchema) && convertToNewIDSchema ) { return convertServerIDsToClientIDs( keyserverPrefixID, outputValidator, data, ); } return data; } function checkInputValidator(inputValidator: TType, input: mixed): T { if (inputValidator.is(input)) { return assertWithValidator(input, inputValidator); } const error = new ServerError('invalid_parameters'); error.sanitizedInput = input ? sanitizeInput(inputValidator, input) : null; throw error; } async function checkClientSupported( viewer: Viewer, inputValidator: ?TType, input: T, ) { let platformDetails; if (inputValidator) { platformDetails = findFirstInputMatchingValidator( inputValidator, tPlatformDetails, input, ); } if (!platformDetails && inputValidator) { const platform = findFirstInputMatchingValidator( inputValidator, tPlatform, input, ); if (platform) { platformDetails = { platform }; } } if (!platformDetails) { ({ platformDetails } = viewer); } await verifyClientSupported(viewer, platformDetails); } const redactedString = '********'; const redactedTypes = [tPassword, tCookie]; function sanitizeInput(inputValidator: TType, input: T): T { return convertObject( inputValidator, input, redactedTypes, () => redactedString, ); } function findFirstInputMatchingValidator( wholeInputValidator: *, inputValidatorToMatch: *, input: *, ): any { if (!wholeInputValidator || input === null || input === undefined) { return null; } if ( wholeInputValidator === inputValidatorToMatch && wholeInputValidator.is(input) ) { return input; } if (wholeInputValidator.meta.kind === 'maybe') { return findFirstInputMatchingValidator( wholeInputValidator.meta.type, inputValidatorToMatch, input, ); } if ( wholeInputValidator.meta.kind === 'interface' && typeof input === 'object' ) { for (const key in input) { const value = input[key]; const validator = wholeInputValidator.meta.props[key]; const innerResult = findFirstInputMatchingValidator( validator, inputValidatorToMatch, value, ); if (innerResult) { return innerResult; } } } if (wholeInputValidator.meta.kind === 'union') { for (const validator of wholeInputValidator.meta.types) { if (validator.is(input)) { return findFirstInputMatchingValidator( validator, inputValidatorToMatch, input, ); } } } if (wholeInputValidator.meta.kind === 'list' && Array.isArray(input)) { const validator = wholeInputValidator.meta.type; for (const value of input) { const innerResult = findFirstInputMatchingValidator( validator, inputValidatorToMatch, value, ); if (innerResult) { return innerResult; } } } return null; } async function policiesValidator( viewer: Viewer, policies: $ReadOnlyArray, ) { if (!policies.length) { return; } if (!hasMinCodeVersion(viewer.platformDetails, { native: 181 })) { return; } const notAcknowledgedPolicies = await fetchNotAcknowledgedPolicies( viewer.id, policies, ); if (notAcknowledgedPolicies.length) { throw new ServerError('policies_not_accepted', { notAcknowledgedPolicies, }); } } export { validateInput, validateOutput, checkInputValidator, redactedString, sanitizeInput, findFirstInputMatchingValidator, checkClientSupported, policiesValidator, }; diff --git a/web/redux/persist.js b/web/redux/persist.js index 95de8268f..ab97a94ed 100644 --- a/web/redux/persist.js +++ b/web/redux/persist.js @@ -1,110 +1,143 @@ // @flow +import invariant from 'invariant'; import { getStoredState, purgeStoredState } from 'redux-persist'; import storage from 'redux-persist/es/storage/index.js'; import type { PersistConfig } from 'redux-persist/src/types.js'; import { createAsyncMigrate, type StorageMigrationFunction, } from 'lib/shared/create-async-migrate.js'; import { isDev } from 'lib/utils/dev-utils.js'; +import { + generateIDSchemaMigrationOpsForDrafts, + convertDraftStoreToNewIDSchema, +} from 'lib/utils/migration-utils.js'; import commReduxStorageEngine from './comm-redux-storage-engine.js'; import type { AppState } from './redux-setup.js'; import { databaseModule } from '../database/database-module-provider.js'; import { isSQLiteSupported } from '../database/utils/db-utils.js'; import { workerRequestMessageTypes } from '../types/worker-types.js'; declare var preloadedState: AppState; const initiallyLoggedInUserID = preloadedState.currentUserInfo?.anonymous ? undefined : preloadedState.currentUserInfo?.id; const isDatabaseSupported = isSQLiteSupported(initiallyLoggedInUserID); const migrations = { [1]: async state => { const { primaryIdentityPublicKey, ...stateWithoutPrimaryIdentityPublicKey } = state; return { ...stateWithoutPrimaryIdentityPublicKey, cryptoStore: { primaryAccount: null, primaryIdentityKeys: null, notificationAccount: null, notificationIdentityKeys: null, }, }; }, [2]: async state => { if (!isDatabaseSupported) { return state; } const { drafts } = state.draftStore; const draftStoreOperations = []; for (const key in drafts) { const text = drafts[key]; draftStoreOperations.push({ type: 'update', payload: { key, text }, }); } await databaseModule.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: { draftStoreOperations }, }); return state; }, + [3]: async (state: AppState) => { + let newState = state; + if (state.draftStore) { + newState = { + ...newState, + draftStore: convertDraftStoreToNewIDSchema(state.draftStore), + }; + } + + if (!isDatabaseSupported) { + return newState; + } + + const stores = await databaseModule.schedule({ + type: workerRequestMessageTypes.GET_CLIENT_STORE, + }); + invariant(stores?.store, 'Stores should exist'); + await databaseModule.schedule({ + type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, + storeOperations: { + draftStoreOperations: generateIDSchemaMigrationOpsForDrafts( + stores.store.drafts, + ), + }, + }); + + return newState; + }, }; const persistWhitelist = [ 'enabledApps', 'deviceID', 'cryptoStore', 'notifPermissionAlertInfo', 'commServicesAccessToken', 'lastCommunicatedPlatformDetails', ]; const rootKey = 'root'; const migrateStorageToSQLite: StorageMigrationFunction = async debug => { const isSupported = await databaseModule.isDatabaseSupported(); if (!isSupported) { return undefined; } const oldStorage = await getStoredState({ storage, key: rootKey }); if (!oldStorage) { return undefined; } purgeStoredState({ storage, key: rootKey }); if (debug) { console.log('redux-persist: migrating state to SQLite storage'); } return oldStorage; }; const persistConfig: PersistConfig = { key: rootKey, storage: commReduxStorageEngine, whitelist: isDatabaseSupported ? persistWhitelist : [...persistWhitelist, 'draftStore'], migrate: (createAsyncMigrate( migrations, { debug: isDev }, migrateStorageToSQLite, ): any), version: 2, }; export { persistConfig };