diff --git a/src/composables/auth.ts b/src/composables/auth.ts index 2ca107afd..3e05bede1 100644 --- a/src/composables/auth.ts +++ b/src/composables/auth.ts @@ -101,7 +101,7 @@ export const useAuth = createCustomScopedComposable(() => { ); /** Part of the `encryptionKey` that is used to encrypt/decrypt protected data */ - const encryptionSalt = useStorageRef( + const encryptionSalt = useStorageRef( null, STORAGE_KEYS.encryptionSalt, { diff --git a/src/composables/balances.ts b/src/composables/balances.ts index 0d0b17a4a..4d02c57bf 100644 --- a/src/composables/balances.ts +++ b/src/composables/balances.ts @@ -16,6 +16,7 @@ import { useStorageRef } from './storageRef'; import { useNetworks } from './networks'; type Balances = Record; +type BalancesSerialized = Record; let composableInitialized = false; @@ -24,7 +25,7 @@ const POLLING_INTERVAL = 5000; const initPollingWatcher = createPollingBasedOnMountedComponents(POLLING_INTERVAL); -const balances = useStorageRef({}, STORAGE_KEYS.balances, { +const balances = useStorageRef({}, STORAGE_KEYS.balances, { serializer: { read: (val) => mapValues(val, (balance: BalanceRaw) => new BigNumber(balance)), write: (val) => mapValues(val, (balance) => balance.toFixed()), diff --git a/src/composables/invites.ts b/src/composables/invites.ts index 181314950..009b930a7 100644 --- a/src/composables/invites.ts +++ b/src/composables/invites.ts @@ -2,7 +2,7 @@ import { AE_AMOUNT_FORMATS, Encoded, } from '@aeternity/aepp-sdk'; -import type { AccountAddress, IInvite } from '@/types'; +import type { AccountAddress, IInvite, IInviteSerialized } from '@/types'; import { STORAGE_KEYS } from '@/constants'; import { tg } from '@/popup/plugins/i18n'; import { getAccountFromSecret } from '@/protocols/aeternity/helpers'; @@ -11,13 +11,20 @@ import { useStorageRef } from './storageRef'; import { useModals } from './modals'; import { useAeSdk } from './aeSdk'; -const invites = useStorageRef( +const invites = useStorageRef( [], STORAGE_KEYS.invites, { migrations: [ migrateInvitesVuexToComposable, ], + serializer: { + read: (arr) => arr + // TODO: remove `as any` after updating `Buffer.from` type + .map(({ secretKey, ...item }) => ({ ...item, secretKey: Buffer.from(secretKey as any) })), + write: (arr) => arr + .map(({ secretKey, ...item }) => ({ ...item, secretKey: secretKey.toJSON() })), + }, }, ); @@ -27,7 +34,7 @@ export function useInvites() { function addInvite(secretKey: Buffer) { invites.value.unshift({ - secretKey: secretKey.toJSON(), + secretKey, createdAt: Date.now(), }); } diff --git a/src/composables/storageRef.ts b/src/composables/storageRef.ts index 752adcc86..b9be2d632 100644 --- a/src/composables/storageRef.ts +++ b/src/composables/storageRef.ts @@ -5,7 +5,7 @@ import { asyncPipe } from '@/utils'; import { SecureMobileStorage } from '@/lib/SecureMobileStorage'; import { IS_MOBILE_APP } from '@/constants'; -export interface ICreateStorageRefOptions { +export interface ICreateStorageRefOptions { /** * Enable secure storage for the data. * Mobile app only. @@ -19,10 +19,10 @@ export interface ICreateStorageRefOptions { * Callbacks run on the data that will be saved and read from the browser storage. */ serializer?: { - read: (val: T) => any; - write: (val: T) => any; + read: (val: TSerialized) => T; + write: (val: T) => TSerialized; }; - migrations?: Migration[]; + migrations?: Migration[]; /** * Allows to ensure the state is already synced with browser storage and migrated. */ @@ -38,14 +38,17 @@ export interface ICreateStorageRefOptions { * Also allows to sync the state between the app and the extension background. * Inspired by `useStorage`: https://vueuse.org/core/useStorage/ */ -export function useStorageRef( +export function useStorageRef( initialState: T, storageKey: StorageKey, - options: ICreateStorageRefOptions = {}, + options: ICreateStorageRefOptions = {}, ) { const { enableSecureStorage = false, - serializer, + serializer = { + read: (a: unknown) => a, + write: (a: unknown) => a, + } as unknown as NonNullable, backgroundSync = false, migrations, onRestored, @@ -56,28 +59,28 @@ export function useStorageRef( const state = ref(initialState) as Ref; // https://github.com/vuejs/core/issues/2136/ const storage = (enableSecureStorage && IS_MOBILE_APP) ? SecureMobileStorage : WalletStorage; - async function setLocalState(val: T | null) { + async function setLocalState(val: TSerialized | null) { if (val !== null) { watcherDisabled = true; - state.value = (serializer?.read) ? await serializer.read(val) : val; + state.value = await serializer.read(val); setTimeout(() => { watcherDisabled = false; }, 0); } } async function setStorageState(val: T | null) { - storage.set(storageKey, (val && serializer?.write) ? await serializer.write(val) : val); + storage.set(storageKey, val ? await serializer.write(val) : val); } // Restore state and run watchers (async () => { - let restoredValue = await storage.get(storageKey); + let restoredValue = await storage.get(storageKey); if (migrations?.length) { - restoredValue = await asyncPipe(migrations)(restoredValue!); + restoredValue = await asyncPipe(migrations)(restoredValue!); if (restoredValue !== null) { - await setStorageState(restoredValue); + storage.set(storageKey, restoredValue); } } - onRestored?.(restoredValue); + onRestored?.(restoredValue == null ? restoredValue as T : await serializer.read(restoredValue)); await setLocalState(restoredValue); /** diff --git a/src/migrations/006-invites-vuex-to-composable.ts b/src/migrations/006-invites-vuex-to-composable.ts index e0da500f5..f21e082aa 100644 --- a/src/migrations/006-invites-vuex-to-composable.ts +++ b/src/migrations/006-invites-vuex-to-composable.ts @@ -1,7 +1,7 @@ -import type { IInvite, Migration } from '@/types'; +import type { IInviteSerialized, Migration } from '@/types'; import { collectVuexState } from './migrationHelpers'; -const migration: Migration = async (restoredValue: IInvite[]) => { +const migration: Migration = async (restoredValue: IInviteSerialized[]) => { if (!restoredValue?.length) { const invites = (await collectVuexState())?.invites?.invites; if (invites?.length) { diff --git a/src/popup/components/InviteItem.vue b/src/popup/components/InviteItem.vue index 53dff7b53..9ab16713c 100644 --- a/src/popup/components/InviteItem.vue +++ b/src/popup/components/InviteItem.vue @@ -162,7 +162,7 @@ export default defineComponent({ const link = computed(() => { // nm_ prefix was chosen as a dummy to decode from base58Check - const secretKey = (encode(Buffer.from(props.secretKey), Encoding.Name)).slice(3); + const secretKey = (encode(props.secretKey, Encoding.Name)).slice(3); return new URL( `${router .resolve({ name: ROUTE_INVITE_CLAIM }) @@ -171,7 +171,7 @@ export default defineComponent({ ); }); - const address = computed(() => getAccountFromSecret(Buffer.from(props.secretKey)).address); + const address = computed(() => getAccountFromSecret(props.secretKey).address); function deleteItem() { removeInvite(props.secretKey); @@ -192,7 +192,7 @@ export default defineComponent({ emit('loading', true); try { await claimInvite({ - secretKey: Buffer.from(props.secretKey), + secretKey: props.secretKey, recipientId: getLastActiveProtocolAccount(PROTOCOLS.aeternity)?.address!, isMax: true, }); diff --git a/src/types/index.ts b/src/types/index.ts index 05d0e7201..d99707081 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -899,10 +899,17 @@ export interface IFormSelectOption { export type Migration = (restoredValue: T | any) => Promise; export interface IInvite { - secretKey: object; + secretKey: Buffer; createdAt: number; } +export interface IInviteSerialized extends Omit { + secretKey: { + type: 'Buffer'; + data: number[]; + }; +} + export interface IHistoryItem { url: string; cleanPath?: string;