diff --git a/src/actions/LocalStorageActions.js b/src/actions/LocalStorageActions.js index 49fbf0fa..786da55c 100644 --- a/src/actions/LocalStorageActions.js +++ b/src/actions/LocalStorageActions.js @@ -10,6 +10,7 @@ import * as PENDING from 'actions/constants/pendingTx'; import * as WALLET from 'actions/constants/wallet'; import { httpRequest } from 'utils/networkUtils'; import * as buildUtils from 'utils/build'; +import * as storageUtils from 'utils/storage'; import { findAccountTokens } from 'reducers/TokensReducer'; import type { Account } from 'reducers/AccountsReducer'; @@ -17,7 +18,6 @@ import type { Token } from 'reducers/TokensReducer'; import type { PendingTx } from 'reducers/PendingTxReducer'; import type { Discovery } from 'reducers/DiscoveryReducer'; - import type { TrezorDevice, ThunkAction, @@ -43,22 +43,15 @@ export type StorageAction = { error: string, }; -const get = (key: string): ?string => { - try { - return window.localStorage.getItem(key); - } catch (error) { - // available = false; - return null; - } -}; - -const set = (key: string, value: string | boolean): void => { - try { - window.localStorage.setItem(key, value); - } catch (error) { - console.error(`Local Storage ERROR: ${error}`); - } -}; +const TYPE: 'local' = 'local'; +const { STORAGE_PATH } = storageUtils; +const KEY_VERSION: string = `${STORAGE_PATH}version`; +const KEY_DEVICES: string = `${STORAGE_PATH}devices`; +const KEY_ACCOUNTS: string = `${STORAGE_PATH}accounts`; +const KEY_DISCOVERY: string = `${STORAGE_PATH}discovery`; +const KEY_TOKENS: string = `${STORAGE_PATH}tokens`; +const KEY_PENDING: string = `${STORAGE_PATH}pending`; +const KEY_BETA_MODAL: string = '/betaModalPrivacy'; // this key needs to be compatible with "parent" (old) wallet // https://github.com/STRML/react-localstorage/blob/master/react-localstorage.js // or @@ -80,25 +73,25 @@ export const save = (): ThunkAction => (dispatch: Dispatch, getState: GetState): const discovery: Array = findDiscovery(devices, getState().discovery); // save devices - set('devices', JSON.stringify(devices)); + storageUtils.set(TYPE, KEY_DEVICES, JSON.stringify(devices)); // save already preloaded accounts - set('accounts', JSON.stringify(accounts)); + storageUtils.set(TYPE, KEY_ACCOUNTS, JSON.stringify(accounts)); // save discovery state - set('discovery', JSON.stringify(discovery)); + storageUtils.set(TYPE, KEY_DISCOVERY, JSON.stringify(discovery)); // tokens - set('tokens', JSON.stringify(tokens)); + storageUtils.set(TYPE, KEY_TOKENS, JSON.stringify(tokens)); // pending transactions - set('pending', JSON.stringify(pending)); + storageUtils.set(TYPE, KEY_PENDING, JSON.stringify(pending)); }; export const update = (event: StorageEvent): ThunkAction => (dispatch: Dispatch): void => { if (!event.newValue) return; - if (event.key === 'devices') { + if (event.key === KEY_DEVICES) { // check if device was added/ removed // const newDevices: Array = JSON.parse(event.newValue); // const myDevices: Array = getState().connect.devices.filter(d => d.features); @@ -113,28 +106,28 @@ export const update = (event: StorageEvent): ThunkAction => (dispatch: Dispatch) // const diff = oldDevices.filter(d => newDevices.indexOf()) } - if (event.key === 'accounts') { + if (event.key === KEY_ACCOUNTS) { dispatch({ type: ACCOUNT.FROM_STORAGE, payload: JSON.parse(event.newValue), }); } - if (event.key === 'tokens') { + if (event.key === KEY_TOKENS) { dispatch({ type: TOKEN.FROM_STORAGE, payload: JSON.parse(event.newValue), }); } - if (event.key === 'pending') { + if (event.key === KEY_PENDING) { dispatch({ type: PENDING.FROM_STORAGE, payload: JSON.parse(event.newValue), }); } - if (event.key === 'discovery') { + if (event.key === KEY_DISCOVERY) { dispatch({ type: DISCOVERY.FROM_STORAGE, payload: JSON.parse(event.newValue), @@ -142,14 +135,13 @@ export const update = (event: StorageEvent): ThunkAction => (dispatch: Dispatch) } }; -const VERSION: string = '1'; - const loadJSON = (): AsyncAction => async (dispatch: Dispatch): Promise => { if (typeof window.localStorage === 'undefined') return; try { const config: Config = await httpRequest(AppConfigJSON, 'json'); + // remove ropsten testnet from config networks if (!buildUtils.isDev()) { const index = config.networks.findIndex(c => c.shortcut === 'trop'); delete config.networks[index]; @@ -161,18 +153,6 @@ const loadJSON = (): AsyncAction => async (dispatch: Dispatch): Promise => dispatch(update(event)); }); - // validate version - const version: ?string = get('version'); - if (version !== VERSION) { - try { - window.localStorage.clear(); - window.sessionStorage.clear(); - } catch (error) { - // empty - } - set('version', VERSION); - } - // load tokens const tokens = await config.networks.reduce(async (promise: Promise, network: Network): Promise => { const collection: TokensCollection = await promise; @@ -195,9 +175,17 @@ const loadJSON = (): AsyncAction => async (dispatch: Dispatch): Promise => } }; +const VERSION: string = '1'; const loadStorageData = (): ThunkAction => (dispatch: Dispatch): void => { - const devices: ?string = get('devices'); + // validate version + const version: ?string = storageUtils.get(TYPE, KEY_VERSION); + if (version && version !== VERSION) { + storageUtils.clearAll(); + } + storageUtils.set(TYPE, KEY_VERSION, VERSION); + + const devices: ?string = storageUtils.get(TYPE, KEY_DEVICES); if (devices) { dispatch({ type: CONNECT.DEVICE_FROM_STORAGE, @@ -205,7 +193,7 @@ const loadStorageData = (): ThunkAction => (dispatch: Dispatch): void => { }); } - const accounts: ?string = get('accounts'); + const accounts: ?string = storageUtils.get(TYPE, KEY_ACCOUNTS); if (accounts) { dispatch({ type: ACCOUNT.FROM_STORAGE, @@ -213,7 +201,7 @@ const loadStorageData = (): ThunkAction => (dispatch: Dispatch): void => { }); } - const userTokens: ?string = get('tokens'); + const userTokens: ?string = storageUtils.get(TYPE, KEY_TOKENS); if (userTokens) { dispatch({ type: TOKEN.FROM_STORAGE, @@ -221,7 +209,7 @@ const loadStorageData = (): ThunkAction => (dispatch: Dispatch): void => { }); } - const pending: ?string = get('pending'); + const pending: ?string = storageUtils.get(TYPE, KEY_PENDING); if (pending) { dispatch({ type: PENDING.FROM_STORAGE, @@ -229,7 +217,7 @@ const loadStorageData = (): ThunkAction => (dispatch: Dispatch): void => { }); } - const discovery: ?string = get('discovery'); + const discovery: ?string = storageUtils.get(TYPE, KEY_DISCOVERY); if (discovery) { dispatch({ type: DISCOVERY.FROM_STORAGE, @@ -238,7 +226,7 @@ const loadStorageData = (): ThunkAction => (dispatch: Dispatch): void => { } if (buildUtils.isDev() || buildUtils.isBeta()) { - const betaModal = get('/betaModalPrivacy'); + const betaModal = Object.keys(window.localStorage).find(key => key.indexOf(KEY_BETA_MODAL) >= 0); if (!betaModal) { dispatch({ type: WALLET.SHOW_BETA_DISCLAIMER, @@ -258,6 +246,6 @@ export const loadData = (): ThunkAction => (dispatch: Dispatch, getState: GetSta }; export const hideBetaDisclaimer = (): ThunkAction => (dispatch: Dispatch): void => { - set('/betaModalPrivacy', true); + storageUtils.set(TYPE, KEY_BETA_MODAL, true); dispatch(loadJSON()); }; diff --git a/src/actions/SessionStorageActions.js b/src/actions/SessionStorageActions.js index 33c953c8..e713a791 100644 --- a/src/actions/SessionStorageActions.js +++ b/src/actions/SessionStorageActions.js @@ -1,5 +1,5 @@ /* @flow */ - +import * as storageUtils from 'utils/storage'; import type { State as SendFormState } from 'reducers/SendFormReducer'; import type { @@ -9,52 +9,39 @@ import type { Dispatch, } from 'flowtype'; -const TX_PREFIX: string = 'trezor:draft-tx:'; +const TYPE: 'session' = 'session'; +const { STORAGE_PATH } = storageUtils; +const KEY_TX_DRAFT: string = `${STORAGE_PATH}txdraft`; + +const getTxDraftKey = (getState: GetState): string => { + const { pathname } = getState().router.location; + return `${KEY_TX_DRAFT}${pathname}`; +}; export const saveDraftTransaction = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - if (typeof window.localStorage === 'undefined') return; - const state = getState().sendForm; if (state.untouched) return; - const location = getState().router.location.pathname; - try { - // save state as it is - // "loadDraftTransaction" will do the validation - window.sessionStorage.setItem(`${TX_PREFIX}${location}`, JSON.stringify(state)); - } catch (error) { - console.error(`Saving sessionStorage error: ${error}`); - } + const key = getTxDraftKey(getState); + storageUtils.set(TYPE, key, JSON.stringify(state)); }; export const loadDraftTransaction = (): PayloadAction => (dispatch: Dispatch, getState: GetState): ?SendFormState => { - if (typeof window.localStorage === 'undefined') return null; - - try { - const location = getState().router.location.pathname; - const value: string = window.sessionStorage.getItem(`${TX_PREFIX}${location}`); - const state: ?SendFormState = JSON.parse(value); - if (state) { - // decide if draft is valid and should be returned - // ignore this draft if has any error - if (Object.keys(state.errors).length > 0) { - window.sessionStorage.removeItem(`${TX_PREFIX}${location}`); - return null; - } - return state; - } - } catch (error) { - console.error(`Loading sessionStorage error: ${error}`); + const key = getTxDraftKey(getState); + const value: ?string = storageUtils.get(TYPE, key); + if (!value) return null; + const state: ?SendFormState = JSON.parse(value); + if (!state) return null; + // decide if draft is valid and should be returned + // ignore this draft if has any error + if (Object.keys(state.errors).length > 0) { + storageUtils.remove(TYPE, key); + return null; } - return null; + return state; }; export const clear = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - if (typeof window.localStorage === 'undefined') return; - const location = getState().router.location.pathname; - try { - window.sessionStorage.removeItem(`${TX_PREFIX}${location}`); - } catch (error) { - console.error(`Clearing sessionStorage error: ${error}`); - } -}; \ No newline at end of file + const key = getTxDraftKey(getState); + storageUtils.remove(TYPE, key); +}; diff --git a/src/utils/storage.js b/src/utils/storage.js new file mode 100644 index 00000000..dd89c7ef --- /dev/null +++ b/src/utils/storage.js @@ -0,0 +1,62 @@ +/* @flow */ + +// Copy-paste from mytrezor (old wallet) +// https://github.com/satoshilabs/mytrezor/blob/develop/app/scripts/storage/BackendLocalStorage.js +export const getStoragePath = (): string => { + const regexHash = /^([^#]*)#.*$/; + const path = window.location.href.replace(regexHash, '$1'); + const regexStart = /^[^:]*:\/\/[^/]*\//; + return path.replace(regexStart, '/'); +}; + +export const STORAGE_PATH: string = getStoragePath(); + +type StorageType = 'local' | 'session'; + +export const get: (type: StorageType, key: string) => T | null = (type, key) => { + const storage = type === 'local' ? window.localStorage : window.sessionStorage; + try { + return storage.getItem(key); + } catch (error) { + console.warn(`Get ${type} storage: ${error}`); + return null; + } +}; + +export const set = (type: StorageType, key: string, value: string | number | boolean): void => { + const storage = type === 'local' ? window.localStorage : window.sessionStorage; + try { + storage.setItem(key, value); + } catch (error) { + console.warn(`Save ${type} storage: ${error}`); + } +}; + +export const remove = (type: StorageType, key: string): void => { + const storage = type === 'local' ? window.localStorage : window.sessionStorage; + try { + storage.removeItem(key); + } catch (error) { + console.warn(`Remove ${type} storage: ${error}`); + } +}; + +export const clearAll = (type: ?StorageType): void => { + let clearLocal: boolean = true; + let clearSession: boolean = true; + if (typeof type === 'string') { + clearLocal = type === 'local'; + clearSession = !clearLocal; + } + + try { + if (clearLocal) { + Object.keys(window.localStorage).forEach(key => key.indexOf(STORAGE_PATH) >= 0 && window.localStorage.removeItem(key)); + } + if (clearSession) { + Object.keys(window.sessionStorage).forEach(key => key.indexOf(STORAGE_PATH) >= 0 && window.sessionStorage.removeItem(key)); + } + } catch (error) { + console.error(`Clearing sessionStorage error: ${error}`); + } +}; \ No newline at end of file