diff --git a/android/gradle.properties b/android/gradle.properties index f06abec2465..75e96754502 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -48,4 +48,3 @@ hermesEnabled=true # TODO: explain following config options android.disableResourceValidation=true android.enableDexingArtifactTransform.desugaring=false -AsyncStorage_db_size_in_MB=10 diff --git a/app/core/Authentication/Authentication.test.ts b/app/core/Authentication/Authentication.test.ts index c3982399a32..05d951415c4 100644 --- a/app/core/Authentication/Authentication.test.ts +++ b/app/core/Authentication/Authentication.test.ts @@ -1,4 +1,4 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; +import AsyncStorage from '../../store/async-storage-wrapper'; import { BIOMETRY_CHOICE_DISABLED, TRUE, @@ -27,7 +27,7 @@ describe('Authentication', () => { }); afterEach(() => { - AsyncStorage.clear(); + AsyncStorage.clearAll(); jest.restoreAllMocks(); }); diff --git a/app/store/async-storage-wrapper.js b/app/store/async-storage-wrapper.js index 7d5aabb639a..32366f2869b 100644 --- a/app/store/async-storage-wrapper.js +++ b/app/store/async-storage-wrapper.js @@ -1,9 +1,11 @@ import ReadOnlyNetworkStore from '../util/test/network-store'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { isE2E } from '../util/test/utils'; +import { MMKV } from 'react-native-mmkv'; /** * Wrapper class for AsyncStorage. + * (Will want to eventuall re-name since no longer async once migratted to mmkv) */ class AsyncStorageWrapper { constructor() { @@ -11,16 +13,21 @@ class AsyncStorageWrapper { * The underlying storage implementation. * Use `ReadOnlyNetworkStore` in test mode otherwise use `AsyncStorage`. */ - this.storage = isE2E ? ReadOnlyNetworkStore : AsyncStorage; + this.storage = isE2E ? ReadOnlyNetworkStore : new MMKV(); } async getItem(key) { try { - return await this.storage.getItem(key); + // asyncStorage returns null for no value + // mmkv returns undefined for no value + // therefore must return null if no value is found + // to keep app behavior consistent + const value = (await this.storage.getString(key)) ?? null; + return value; } catch (error) { if (isE2E) { // Fall back to AsyncStorage in test mode if ReadOnlyNetworkStore fails - return AsyncStorage.getItem(key); + return await AsyncStorage.getItem(key); } throw error; } @@ -28,7 +35,11 @@ class AsyncStorageWrapper { async setItem(key, value) { try { - return await this.storage.setItem(key, value); + if (typeof value !== 'string') + throw new Error( + `MMKV value must be a string, received value ${value} for key ${key}`, + ); + return await this.storage.set(key, value); } catch (error) { if (isE2E) { // Fall back to AsyncStorage in test mode if ReadOnlyNetworkStore fails @@ -40,7 +51,7 @@ class AsyncStorageWrapper { async removeItem(key) { try { - return await this.storage.removeItem(key); + return await this.storage.delete(key); } catch (error) { if (isE2E) { // Fall back to AsyncStorage in test mode if ReadOnlyNetworkStore fails @@ -49,6 +60,10 @@ class AsyncStorageWrapper { throw error; } } + + async clearAll() { + await this.storage.clearAll(); + } } export default new AsyncStorageWrapper(); diff --git a/app/store/migrations/049.test.ts b/app/store/migrations/049.test.ts new file mode 100644 index 00000000000..19af5ca140f --- /dev/null +++ b/app/store/migrations/049.test.ts @@ -0,0 +1,31 @@ +import migrate, { storage as mmkvStorage } from './049'; +import AsyncStorage from '@react-native-async-storage/async-storage'; + +const asyncStorageItems: { [key: string]: string } = { + valueA: 'a', + valueB: 'true', + valueC: 'myValue', +}; + +describe('Migration #49', () => { + it('migrates asyncStorage values to mmkv ', async () => { + // set asyncStorageItems to AsyncStorage + for (const key in asyncStorageItems) { + await AsyncStorage.setItem(key, asyncStorageItems[key]); + } + + await migrate({}); + + // make sure all AsyncStorage items are removed + const keys = await AsyncStorage.getAllKeys(); + // loop through all AsyncStorage keys and make sure empty + for (const key of keys) { + expect(await AsyncStorage.getItem(key)).toBeNull(); + } + + // now check that all MMKV values match original AsyncStorage values + for (const key in asyncStorageItems) { + expect(mmkvStorage.getString(key)).toEqual(asyncStorageItems[key]); + } + }); +}); diff --git a/app/store/migrations/049.ts b/app/store/migrations/049.ts new file mode 100644 index 00000000000..cc20ebd750b --- /dev/null +++ b/app/store/migrations/049.ts @@ -0,0 +1,25 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { captureException } from '@sentry/react-native'; +import { MMKV } from 'react-native-mmkv'; + +export const storage = new MMKV(); + +export default async function migrate(state: unknown) { + const keys = await AsyncStorage.getAllKeys(); + for (const key of keys) { + try { + const value = await AsyncStorage.getItem(key); + + if (value != null) { + storage.set(key, value); + } + await AsyncStorage.removeItem(key); + } catch (error) { + captureException( + `Failed to migrate key "${key}" from AsyncStorage to MMKV! Error: ${error}`, + ); + } + } + + return state; +} diff --git a/app/store/migrations/index.ts b/app/store/migrations/index.ts index 9e1131237fd..bfdbce04cb8 100644 --- a/app/store/migrations/index.ts +++ b/app/store/migrations/index.ts @@ -49,6 +49,7 @@ import migration45 from './045'; import migration46 from './046'; import migration47 from './047'; import migration48 from './048'; +import migration49 from './049'; type MigrationFunction = (state: unknown) => unknown; type AsyncMigrationFunction = (state: unknown) => Promise; @@ -110,6 +111,7 @@ export const migrationList: MigrationsList = { 46: migration46, 47: migration47, 48: migration48, + 49: migration49, }; // Enable both synchronous and asynchronous migrations diff --git a/app/util/test/network-store.js b/app/util/test/network-store.js index b3870d9ed2a..1475cc40abe 100644 --- a/app/util/test/network-store.js +++ b/app/util/test/network-store.js @@ -46,18 +46,18 @@ class ReadOnlyNetworkStore { } // Async Storage - async getItem(key) { + async getString(key) { await this._initIfRequired(); const value = this._asyncState[key]; return value !== undefined ? value : null; } - async setItem(key, value) { + async set(key, value) { await this._initIfRequired(); this._asyncState[key] = value; } - async removeItem(key) { + async delete(key) { await this._initIfRequired(); delete this._asyncState[key]; }