diff --git a/.changeset/fresh-cows-repair.md b/.changeset/fresh-cows-repair.md new file mode 100644 index 000000000..84a87137d --- /dev/null +++ b/.changeset/fresh-cows-repair.md @@ -0,0 +1,5 @@ +--- +"@onflow/fcl-core": minor +--- + +Add getStorageProvider to currentUser configuration diff --git a/.changeset/slow-peaches-vanish.md b/.changeset/slow-peaches-vanish.md new file mode 100644 index 000000000..ac02855d1 --- /dev/null +++ b/.changeset/slow-peaches-vanish.md @@ -0,0 +1,5 @@ +--- +"@onflow/fcl": minor +--- + +Use localStorage as default & export LOCAL_STORAGE/SESSION_STORAGE as helpers for fcl.storage.default configuration key diff --git a/.changeset/stupid-turkeys-hope.md b/.changeset/stupid-turkeys-hope.md new file mode 100644 index 000000000..b7f1e70f1 --- /dev/null +++ b/.changeset/stupid-turkeys-hope.md @@ -0,0 +1,5 @@ +--- +"@onflow/fcl-react-native": patch +--- + +Pass getStorageProvider to currentUser configuration diff --git a/package-lock.json b/package-lock.json index e9f058c21..a9092608d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30061,13 +30061,13 @@ }, "packages/fcl": { "name": "@onflow/fcl", - "version": "1.12.4-alpha.3", + "version": "1.12.4-alpha.5", "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.25.7", "@onflow/config": "1.5.1-alpha.0", - "@onflow/fcl-core": "1.13.0-alpha.2", - "@onflow/fcl-wc": "5.4.1-alpha.1", + "@onflow/fcl-core": "1.13.0-alpha.3", + "@onflow/fcl-wc": "5.4.1-alpha.3", "@onflow/interaction": "0.0.11", "@onflow/rlp": "1.2.3-alpha.0", "@onflow/sdk": "1.5.4-alpha.1", @@ -30131,7 +30131,7 @@ }, "packages/fcl-core": { "name": "@onflow/fcl-core", - "version": "1.13.0-alpha.2", + "version": "1.13.0-alpha.3", "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.25.7", @@ -30180,12 +30180,12 @@ }, "packages/fcl-react-native": { "name": "@onflow/fcl-react-native", - "version": "1.9.7-alpha.2", + "version": "1.9.7-alpha.3", "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.25.7", "@onflow/config": "1.5.1-alpha.0", - "@onflow/fcl-core": "1.13.0-alpha.2", + "@onflow/fcl-core": "1.13.0-alpha.3", "@onflow/interaction": "0.0.11", "@onflow/rlp": "1.2.3-alpha.0", "@onflow/sdk": "1.5.4-alpha.1", @@ -30233,7 +30233,7 @@ }, "packages/fcl-wc": { "name": "@onflow/fcl-wc", - "version": "5.4.1-alpha.1", + "version": "5.4.1-alpha.3", "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.25.7", @@ -30253,7 +30253,7 @@ "jest": "^29.7.0" }, "peerDependencies": { - "@onflow/fcl-core": "1.13.0-alpha.2" + "@onflow/fcl-core": "1.13.0-alpha.3" } }, "packages/fcl/node_modules/typescript": { diff --git a/packages/fcl-core/src/current-user/index.js b/packages/fcl-core/src/current-user/index.js index b1a87c33b..58248ccbc 100644 --- a/packages/fcl-core/src/current-user/index.js +++ b/packages/fcl-core/src/current-user/index.js @@ -1,6 +1,6 @@ import "../default-config" import * as t from "@onflow/types" -import {account, arg} from "@onflow/sdk" +import {arg} from "@onflow/sdk" import {config} from "@onflow/config" import {spawn, send, INIT, SUBSCRIBE, UNSUBSCRIBE} from "@onflow/util-actor" import {withPrefix, sansPrefix} from "@onflow/util-address" @@ -46,53 +46,61 @@ const getStoredUser = async storage => { return stored || fallback } -const HANDLERS = { - [INIT]: async ctx => { - if (typeof window === "undefined") { - console.warn( - ` +const makeHandlers = cfg => { + // Wrapper for backwards compatibility + const getStorageProvider = async () => { + if (cfg.getStorageProvider) return await cfg.getStorageProvider() + return await config.first(["fcl.storage", "fcl.storage.default"]) + } + + return { + [INIT]: async ctx => { + if (typeof window === "undefined") { + console.warn( + ` %cFCL Warning ============================ "currentUser" is only available in the browser. For more info, please see the docs: https://docs.onflow.org/fcl/ ============================ `, - "font-weight:bold;font-family:monospace;" - ) - } + "font-weight:bold;font-family:monospace;" + ) + } - ctx.merge(JSON.parse(DATA)) - const storage = await config.first(["fcl.storage", "fcl.storage.default"]) - if (storage.can) { - const user = await getStoredUser(storage) - if (notExpired(user)) ctx.merge(user) - } - }, - [SUBSCRIBE]: (ctx, letter) => { - ctx.subscribe(letter.from) - ctx.send(letter.from, UPDATED, {...ctx.all()}) - }, - [UNSUBSCRIBE]: (ctx, letter) => { - ctx.unsubscribe(letter.from) - }, - [SNAPSHOT]: async (ctx, letter) => { - letter.reply({...ctx.all()}) - }, - [SET_CURRENT_USER]: async (ctx, letter, data) => { - ctx.merge(data) - const storage = await config.first(["fcl.storage", "fcl.storage.default"]) - if (storage.can) storage.put(NAME, ctx.all()) - ctx.broadcast(UPDATED, {...ctx.all()}) - }, - [DEL_CURRENT_USER]: async (ctx, letter) => { - ctx.merge(JSON.parse(DATA)) - const storage = await config.first(["fcl.storage", "fcl.storage.default"]) - if (storage.can) storage.put(NAME, ctx.all()) - ctx.broadcast(UPDATED, {...ctx.all()}) - }, + ctx.merge(JSON.parse(DATA)) + const storage = await getStorageProvider() + if (storage.can) { + const user = await getStoredUser(storage) + if (notExpired(user)) ctx.merge(user) + } + }, + [SUBSCRIBE]: (ctx, letter) => { + ctx.subscribe(letter.from) + ctx.send(letter.from, UPDATED, {...ctx.all()}) + }, + [UNSUBSCRIBE]: (ctx, letter) => { + ctx.unsubscribe(letter.from) + }, + [SNAPSHOT]: async (ctx, letter) => { + letter.reply({...ctx.all()}) + }, + [SET_CURRENT_USER]: async (ctx, letter, data) => { + ctx.merge(data) + const storage = await getStorageProvider() + if (storage.can) storage.put(NAME, ctx.all()) + ctx.broadcast(UPDATED, {...ctx.all()}) + }, + [DEL_CURRENT_USER]: async (ctx, letter) => { + ctx.merge(JSON.parse(DATA)) + const storage = await getStorageProvider() + if (storage.can) storage.put(NAME, ctx.all()) + ctx.broadcast(UPDATED, {...ctx.all()}) + }, + } } -const spawnCurrentUser = () => spawn(HANDLERS, NAME) +const spawnCurrentUser = cfg => spawn(makeHandlers(cfg), NAME) function notExpired(user) { return ( @@ -148,18 +156,14 @@ const makeConfig = async ({ /** * @description - Factory function to get the authenticate method - * @param {object} [opts] - Options - * @param {object} [opts.platform] - platform that runs the function - * @param {object} [opts.service] - Optional service to use for authentication - * @param {object} [opts.discovery] - Optional discovery options + * @param {CurrentUserConfig} config - Current User Configuration */ const getAuthenticate = - ({platform, discovery}) => + config => /** * @description - Authenticate a user * @param {object} [opts] - Options * @param {object} [opts.service] - Optional service to use for authentication - * @param {object} [opts.user] - Optional user object * @param {boolean} [opts.redir] - Optional redirect flag * @returns */ @@ -174,9 +178,9 @@ const getAuthenticate = } return new Promise(async (resolve, reject) => { - spawnCurrentUser() + spawnCurrentUser(config) const opts = {redir} - const user = await snapshot() + const user = await getSnapshot(config)() const discoveryService = await getDiscoveryService(service) const refreshService = serviceOfType(user.services, "authn-refresh") let accountProofData @@ -188,7 +192,7 @@ const getAuthenticate = service: refreshService, msg: accountProofData, opts, - platform, + platform: config.platform, user, }) send(NAME, SET_CURRENT_USER, await buildUser(response)) @@ -199,7 +203,7 @@ const getAuthenticate = level: LEVELS.error, }) } finally { - return resolve(await snapshot()) + return resolve(await getSnapshot(config)()) } } else { return resolve(user) @@ -223,8 +227,8 @@ const getAuthenticate = msg: accountProofData, config: await makeConfig(discoveryService), opts, - platform, - execStrategy: discovery?.execStrategy, + platform: config.platform, + execStrategy: config.discovery?.execStrategy, user, }) @@ -236,18 +240,23 @@ const getAuthenticate = level: LEVELS.error, }) } finally { - resolve(await snapshot()) + resolve(await getSnapshot(config)()) } }) } /** - * @description - Unauthenticate a user - * @returns {void} + * @description - Factory function to get the unauthenticate method + * @param {CurrentUserConfig} config - Current User Configuration */ -function unauthenticate() { - spawnCurrentUser() - send(NAME, DEL_CURRENT_USER) +function getUnauthenticate(config) { + /** + * @description - Unauthenticate a user + */ + return function unauthenticate() { + spawnCurrentUser(config) + send(NAME, DEL_CURRENT_USER) + } } const normalizePreAuthzResponse = authz => ({ @@ -258,8 +267,12 @@ const normalizePreAuthzResponse = authz => ({ authorization: (authz || {}).authorization || [], }) +/** + * @description - Factory function to get the resolvePreAuthz method + * @param {CurrentUserConfig} config - Current User Configuration + */ const getResolvePreAuthz = - ({platform}) => + config => (authz, {user}) => { const resp = normalizePreAuthzResponse(authz) const axs = [] @@ -276,7 +289,7 @@ const getResolvePreAuthz = return execService({ service: az, msg: signable, - platform, + platform: config.platform, user, }) }, @@ -290,35 +303,36 @@ const getResolvePreAuthz = } /** - * @description - * Produces the needed authorization details for the current user to submit transactions to Flow - * It defines a signing function that connects to a user's wallet provider to produce signatures to submit transactions. + * @description - Factory function to get the authorization method * - * @param {object} opts - running options - * @param {string} opts.platform - platform that runs the function - * @param {object} [opts.discovery] - discovery options - * @param {object} account - Account object - * @returns {Promise} - Account object with signing function + * @param {CurrentUserConfig} config - Current User Configuration */ const getAuthorization = - ({platform, discovery}) => + config => + /** + * @description - Produces the needed authorization details for the current user to submit transactions to Flow + * It defines a signing function that connects to a user's wallet provider to produce signatures to submit transactions. + * + * @param {object} account - Account object + * @returns {Promise} - Account object with signing function + * */ async account => { - spawnCurrentUser() + spawnCurrentUser(config) return { ...account, tempId: "CURRENT_USER", async resolve(account, preSignable) { - const user = await getAuthenticate({platform, discovery})({redir: true}) + const user = await getAuthenticate(config)({redir: true}) const authz = serviceOfType(user.services, "authz") const preAuthz = serviceOfType(user.services, "pre-authz") if (preAuthz) - return getResolvePreAuthz({platform, discovery})( + return getResolvePreAuthz(config)( await execService({ service: preAuthz, msg: preSignable, - platform, + platform: config.platform, user, }), { @@ -342,7 +356,7 @@ const getAuthorization = opts: { includeOlderJsonRpcCall: true, }, - platform, + platform: config.platform, user, }) ) @@ -357,59 +371,59 @@ const getAuthorization = } /** - * @description - * The callback passed to subscribe will be called when the user authenticates and un-authenticates, making it easy to update the UI accordingly. - * - * @param {Function} callback - Callback function - * @returns {Function} - Unsubscribe function + * @description - Factory function to get the subscribe method + * @param {CurrentUserConfig} config - Current User Configuration */ -function subscribe(callback) { - spawnCurrentUser() - const EXIT = "@EXIT" - const self = spawn(async ctx => { - ctx.send(NAME, SUBSCRIBE) - while (1) { - const letter = await ctx.receive() - if (letter.tag === EXIT) { - ctx.send(NAME, UNSUBSCRIBE) - return +function getSubscribe(config) { + /** + * @description + * The callback passed to subscribe will be called when the user authenticates and un-authenticates, making it easy to update the UI accordingly. + * + * @param {Function} callback - Callback function + * @returns {Function} - Unsubscribe function + */ + return function subscribe(callback) { + spawnCurrentUser(config) + const EXIT = "@EXIT" + const self = spawn(async ctx => { + ctx.send(NAME, SUBSCRIBE) + while (1) { + const letter = await ctx.receive() + if (letter.tag === EXIT) { + ctx.send(NAME, UNSUBSCRIBE) + return + } + callback(letter.data) } - callback(letter.data) - } - }) - return () => send(self, EXIT) + }) + return () => send(self, EXIT) + } } /** - * @description - Gets the current user - * @returns {Promise} - User object + * @description - Factory function to get the snapshot method + * @param {CurrentUserConfig} config - Current User Configuration */ -function snapshot() { - spawnCurrentUser() - return send(NAME, SNAPSHOT, null, {expectReply: true, timeout: 0}) -} - -async function info() { - spawnCurrentUser() - const {addr} = await snapshot() - if (addr == null) throw new Error("No Flow Address for Current User") - return account(addr) +function getSnapshot(config) { + /** + * @description - Gets the current user + * @returns {Promise} - User object + */ + return function snapshot() { + spawnCurrentUser(config) + return send(NAME, SNAPSHOT, null, {expectReply: true, timeout: 0}) + } } /** - * @description - Resolves the current user as an argument + * Resolves the current user as an argument + * @param {CurrentUserConfig} config - Current User Configuration * - * @param {object} opts - running options - * @param {string} opts.platform - platform that runs the function - * @param {object} [opts.discovery] - discovery options - * @returns {Promise} */ -const getResolveArgument = - ({platform, discovery}) => - async () => { - const {addr} = await getAuthenticate({platform, discovery})() - return arg(withPrefix(addr), t.Address) - } +const getResolveArgument = config => async () => { + const {addr} = await getAuthenticate(config)() + return arg(withPrefix(addr), t.Address) +} const makeSignable = msg => { invariant(/^[0-9a-f]+$/i.test(msg), "Message must be a hex string") @@ -421,21 +435,18 @@ const makeSignable = msg => { /** * @description - Factory function to get the signUserMessage method - * @param {object} opts - running options - * @param {string} opts.platform - platform that runs the function - * @param {object} [opts.discovery] - discovery options - * @returns {function(string): Promise} + * @param {CurrentUserConfig} config - Current User Configuration */ const getSignUserMessage = - ({platform, discovery}) => + config => /** * @description - A method to use allowing the user to personally sign data via FCL Compatible Wallets/Services. * @param {string} msg - Message to sign * @returns {Promise} - Array of CompositeSignatures */ async msg => { - spawnCurrentUser() - const user = await getAuthenticate({platform, discovery})({ + spawnCurrentUser(config) + const user = await getAuthenticate(config)({ redir: true, }) @@ -450,7 +461,7 @@ const getSignUserMessage = const response = await execService({ service: signingService, msg: makeSignable(msg), - platform, + platform: config.platform, user, }) if (Array.isArray(response)) { @@ -463,45 +474,36 @@ const getSignUserMessage = } } +/** + * @typedef {object} CurrentUserConfig - Current User Configuration + * @property {string} platform - Platform + * @property {object} [discovery] - FCL Discovery Configuration + * @property {() => Promise} [getStorageProvider] - Storage Provider Getter + */ + /** * @description * Creates the Current User object * - * @param {object} opts - Configuration Options - * @param {string} opts.platform - Platform - * @param {object} [opts.discovery] - Discovery Config Resolver for additional configuration - */ -const getCurrentUser = ({platform, discovery}) => { - let currentUser = () => { - return { - authenticate: getAuthenticate({platform, discovery}), - unauthenticate, - authorization: getAuthorization({platform, discovery}), - signUserMessage: getSignUserMessage({platform, discovery}), - subscribe, - snapshot, - resolveArgument: getResolveArgument({platform, discovery}), - } + * @param {CurrentUserConfig} config - Current User Configuration + * */ +const getCurrentUser = config => { + const currentUser = { + authenticate: getAuthenticate(config), + unauthenticate: getUnauthenticate(config), + authorization: getAuthorization(config), + signUserMessage: getSignUserMessage(config), + subscribe: getSubscribe(config), + snapshot: getSnapshot(config), + resolveArgument: getResolveArgument(config), } - currentUser.authenticate = getAuthenticate({ - platform, - discovery, - }) - currentUser.unauthenticate = unauthenticate - currentUser.authorization = getAuthorization({ - platform, - discovery, - }) - currentUser.signUserMessage = getSignUserMessage({ - platform, - discovery, - }) - currentUser.subscribe = subscribe - currentUser.snapshot = snapshot - currentUser.resolveArgument = getResolveArgument({platform, discovery}) - - return currentUser + return Object.assign( + () => { + return {...currentUser} + }, + {...currentUser} + ) } export {getCurrentUser} diff --git a/packages/fcl-core/src/fcl-core.ts b/packages/fcl-core/src/fcl-core.ts index ef6579566..a031b2ca5 100644 --- a/packages/fcl-core/src/fcl-core.ts +++ b/packages/fcl-core/src/fcl-core.ts @@ -148,3 +148,5 @@ export { } from "./utils/constants" export {execStrategy} from "./current-user/exec-service" + +export type {StorageProvider} from "./utils/storage" diff --git a/packages/fcl-core/src/utils/storage.ts b/packages/fcl-core/src/utils/storage.ts new file mode 100644 index 000000000..a477becfd --- /dev/null +++ b/packages/fcl-core/src/utils/storage.ts @@ -0,0 +1,5 @@ +export type StorageProvider = { + can: boolean + get: (key: string) => Promise + put: (key: string, value: any) => Promise +} diff --git a/packages/fcl-react-native/src/fcl-react-native.ts b/packages/fcl-react-native/src/fcl-react-native.ts index 061a07096..11357a90e 100644 --- a/packages/fcl-react-native/src/fcl-react-native.ts +++ b/packages/fcl-react-native/src/fcl-react-native.ts @@ -71,7 +71,12 @@ import { } from "@onflow/fcl-core" export const mutate = getMutate({platform: "react-native"}) -const currentUser = getCurrentUser({platform: "react-native"}) +const currentUser = getCurrentUser({ + platform: "react-native", + getStorageProvider: async () => { + return (await config().get("fcl.storage")) || getAsyncStorage() + }, +}) export {currentUser} @@ -93,6 +98,7 @@ import { useServiceDiscovery, ServiceDiscovery, } from "./utils/react-native" +import {getAsyncStorage} from "./utils/react-native/storage" config(getDefaultConfig()) diff --git a/packages/fcl-react-native/src/utils/react-native/default-config.js b/packages/fcl-react-native/src/utils/react-native/default-config.js index 8c487b152..5edeca7de 100644 --- a/packages/fcl-react-native/src/utils/react-native/default-config.js +++ b/packages/fcl-react-native/src/utils/react-native/default-config.js @@ -1,22 +1,5 @@ -import AsyncStorage from "@react-native-async-storage/async-storage" - -const getAsyncStorage = () => { - try { - const ASYNC_STORAGE = { - can: true, - get: async key => JSON.parse(await AsyncStorage.getItem(key)), - put: async (key, value) => - await AsyncStorage.setItem(key, JSON.stringify(value)), - } - return ASYNC_STORAGE - } catch (error) { - return null - } -} - export const getDefaultConfig = () => { return { "discovery.wallet.method.default": "DEEPLINK/RPC", - "fcl.storage.default": getAsyncStorage(), } } diff --git a/packages/fcl-react-native/src/utils/react-native/storage.ts b/packages/fcl-react-native/src/utils/react-native/storage.ts new file mode 100644 index 000000000..e835cf1e0 --- /dev/null +++ b/packages/fcl-react-native/src/utils/react-native/storage.ts @@ -0,0 +1,25 @@ +import AsyncStorage from "@react-native-async-storage/async-storage" + +const safeParseJSON = (str?: string | null) => { + if (str == null) return null + try { + return JSON.parse(str) + } catch (error) { + return null + } +} + +export const getAsyncStorage = () => { + try { + const ASYNC_STORAGE = { + can: true, + get: async (key: string) => + safeParseJSON(await AsyncStorage.getItem(key)), + put: async (key: string, value: any) => + await AsyncStorage.setItem(key, JSON.stringify(value)), + } + return ASYNC_STORAGE + } catch (error) { + return null + } +} diff --git a/packages/fcl/src/fcl.ts b/packages/fcl/src/fcl.ts index 79f5936a4..35da87833 100644 --- a/packages/fcl/src/fcl.ts +++ b/packages/fcl/src/fcl.ts @@ -63,7 +63,12 @@ export { nodeVersionInfo, } from "@onflow/fcl-core" -import {getMutate, getCurrentUser, initServiceRegistry} from "@onflow/fcl-core" +import { + getMutate, + getCurrentUser, + initServiceRegistry, + StorageProvider, +} from "@onflow/fcl-core" import {execStrategyHook} from "./discovery/exec-hook" const discoveryOpts = { @@ -74,6 +79,11 @@ export const mutate = getMutate({platform: "web", discovery: discoveryOpts}) export const currentUser = getCurrentUser({ platform: "web", discovery: discoveryOpts, + getStorageProvider: async () => { + return ( + (await config.get("fcl.storage")) || LOCAL_STORAGE + ) + }, }) export const authenticate = (opts = {}) => currentUser().authenticate(opts) @@ -88,7 +98,7 @@ export const logIn = (opts = {}) => currentUser().authenticate(opts) export const authz = currentUser().authorization import {config} from "@onflow/config" -import {getDefaultConfig, coreStrategies} from "./utils/web" +import {getDefaultConfig, coreStrategies, LOCAL_STORAGE} from "./utils/web" import {initFclWcLoader} from "./utils/walletconnect/loader" config(getDefaultConfig()) @@ -98,3 +108,5 @@ initServiceRegistry({coreStrategies}) // Automatically load fcl-wc plugin // Based on the user's config initFclWcLoader() + +export {LOCAL_STORAGE, SESSION_STORAGE} from "./utils/web" diff --git a/packages/fcl/src/utils/web/default-config.js b/packages/fcl/src/utils/web/default-config.js index 02c783eac..5127b7128 100644 --- a/packages/fcl/src/utils/web/default-config.js +++ b/packages/fcl/src/utils/web/default-config.js @@ -1,22 +1,5 @@ -const isServerSide = () => typeof window === "undefined" - -const getSessionStorage = () => { - try { - const SESSION_STORAGE = { - can: !isServerSide(), - get: async key => JSON.parse(sessionStorage.getItem(key)), - put: async (key, value) => - sessionStorage.setItem(key, JSON.stringify(value)), - } - return SESSION_STORAGE - } catch (error) { - return null - } -} - export const getDefaultConfig = () => { return { "discovery.wallet.method.default": "IFRAME/RPC", - "fcl.storage.default": getSessionStorage(), } } diff --git a/packages/fcl/src/utils/web/index.js b/packages/fcl/src/utils/web/index.js index 86d47b3c9..7c943c9f8 100644 --- a/packages/fcl/src/utils/web/index.js +++ b/packages/fcl/src/utils/web/index.js @@ -3,3 +3,4 @@ export {renderPop} from "./render-pop" export {renderTab} from "./render-tab" export {getDefaultConfig} from "./default-config" export {coreStrategies} from "./coreStrategies" +export {LOCAL_STORAGE, SESSION_STORAGE} from "./storage" diff --git a/packages/fcl/src/utils/web/storage.ts b/packages/fcl/src/utils/web/storage.ts new file mode 100644 index 000000000..88da9a5c9 --- /dev/null +++ b/packages/fcl/src/utils/web/storage.ts @@ -0,0 +1,25 @@ +import {StorageProvider} from "@onflow/fcl-core" + +const isServerSide = () => typeof window === "undefined" +const safeParseJSON = (str?: string | null) => { + if (str == null) return null + try { + return JSON.parse(str) + } catch (error) { + return null + } +} + +export const SESSION_STORAGE = { + can: !isServerSide() && !!window.sessionStorage, + get: async (key: string) => safeParseJSON(sessionStorage.getItem(key)), + put: async (key: string, value: any) => + sessionStorage.setItem(key, JSON.stringify(value)), +} as StorageProvider + +export const LOCAL_STORAGE = { + can: !isServerSide() && !!window.localStorage, + get: async (key: string) => safeParseJSON(localStorage.getItem(key)), + put: async (key: string, value: any) => + localStorage.setItem(key, JSON.stringify(value)), +} as StorageProvider