Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature(unlock-app): Improve localStorage usage #15361

Merged
merged 8 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions unlock-app/src/components/content/DemoContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ const usePaywall = () => {
},
}

// Remove localStorage (on the demo we do not want to store any account)
localStorage.removeItem('userInfo')

// Event handler
const handler = window.addEventListener(
'unlockProtocol.status',
Expand Down
55 changes: 36 additions & 19 deletions unlock-app/src/hooks/useAppStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,58 +2,75 @@ import { useCallback } from 'react'

export const APP_NAME = '@unlock-app'

const isObject = (value: any) => typeof value === 'object'
/**
* Store objects as JSON, so it's easier to just always try parse, then fallback to the raw value on parse errors.
*/
function parseIfNeeded(value: string) {
try {
return JSON.parse(value)
} catch {
// If not valid JSON, just return the original string
return value
}
}

const getKey = (key: string, withAppName = true) => {
function getKey(key: string, withAppName = true) {
return withAppName ? `${APP_NAME}.${key}` : key
}

export const getLocalStorageItem = (key: string): string | null => {
export function getLocalStorageItem(key: string, withAppName = true) {
if (typeof window === 'undefined') return null
try {
const value = localStorage.getItem(getKey(key))
if (!value) return null
return isObject(value) ? JSON.parse(value) : value
const stored = localStorage.getItem(getKey(key, withAppName))
if (!stored) return null
return parseIfNeeded(stored)
} catch (error) {
console.error(error)
return null
}
return null
}

export const setLocalStorageItem = (key: string, value: any) => {
const currentValue = getLocalStorageItem(key)
if (currentValue === value) return false
export function setLocalStorageItem(
key: string,
value: any,
withAppName = true
) {
try {
localStorage.setItem(
getKey(key),
isObject(value) ? JSON.stringify(value) : value
getKey(key, withAppName),
typeof value === 'object' ? JSON.stringify(value) : String(value)
)
return value
} catch (error) {
console.error(error)
return null
}
return value
}

export const deleteLocalStorageItem = (key: string, withAppName = true) => {
export function deleteLocalStorageItem(key: string, withAppName = true) {
try {
localStorage.removeItem(getKey(key, withAppName))
} catch (error) {
console.error(error)
}
}

export const clearLocalStorage = (clearItems: string[], addAppName = true) => {
for (const item of clearItems) {
deleteLocalStorageItem(item, addAppName)
export function clearLocalStorage(keys: string[], withAppName = true) {
for (const key of keys) {
deleteLocalStorageItem(key, withAppName)
}
}

export function useAppStorage() {
// Get and set items in local storage, with caching!
const getStorage = useCallback(getLocalStorageItem, [])
const setStorage = useCallback(setLocalStorageItem, [])
const removeKey = useCallback(deleteLocalStorageItem, [])
const clearStorage = useCallback(clearLocalStorage, [])

return {
setStorage,
getStorage,
setStorage,
removeKey,
clearStorage,
}
}
26 changes: 10 additions & 16 deletions unlock-app/src/hooks/useTermsOfService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,29 @@ import { useAppStorage } from './useAppStorage'

export const localStorageKey = 'terms-of-service'

/**
* This hook retrieves metadata for a token
* @param {*} address
*/
export const useTermsOfService = () => {
const [termsLoading, setTermsLoading] = useState(true)
const [termsAccepted, setTermsAccepted] = useState(false)
const { getStorage, setStorage } = useAppStorage()

useEffect(() => {
const readFromLocalStorage = () => {
try {
setTermsAccepted(getStorage(localStorageKey) === 'true')
setTermsLoading(false)
} catch (error) {
// No localstorage, assume false!
setTermsAccepted(false)
setTermsLoading(false)
}
try {
const storedVal = getStorage(localStorageKey)
setTermsAccepted(storedVal === true)
} catch (error) {
console.error(error)
setTermsAccepted(false)
} finally {
setTermsLoading(false)
}
readFromLocalStorage()
}, [getStorage])

const saveTermsAccepted = () => {
setTermsAccepted(true)
try {
setStorage(localStorageKey, 'true')
setStorage(localStorageKey, true)
} catch (error) {
// Could not store in localstorage.
console.error('Could not store TOS', error)
}
}

Expand Down
28 changes: 15 additions & 13 deletions unlock-app/src/utils/session.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
import {
APP_NAME,
deleteLocalStorageItem,
getLocalStorageItem,
setLocalStorageItem,
deleteLocalStorageItem,
} from '~/hooks/useAppStorage'

export const CURRENT_ACCOUNT_KEY = `${APP_NAME}.account`
/**
* These constants are *not* prefixed with "@unlock-app."
* They'll be prefixed automatically.
*/
const CURRENT_ACCOUNT_KEY = 'account'
const PROVIDER_KEY = 'provider'
const NETWORK_KEY = 'network'

export const getSessionKey = (address: string) =>
`${APP_NAME}.session_${address.trim().toLowerCase()}`
`session_${address.trim().toLowerCase()}`

export const getCurrentAccount = () => {
return getLocalStorageItem(CURRENT_ACCOUNT_KEY) || undefined
}

export const getCurrentProvider = () => {
return getLocalStorageItem(`${APP_NAME}.provider`)
return getLocalStorageItem(PROVIDER_KEY)
}

export const getCurrentNetwork = () => {
const network = getLocalStorageItem(`${APP_NAME}.network`)
const network = getLocalStorageItem(NETWORK_KEY)
return network ? parseInt(network) : undefined
}

Expand All @@ -29,25 +34,22 @@ export const getAccessToken = (
if (!address) {
return null
}
const ACCESS_TOKEN_KEY = getSessionKey(address)
return getLocalStorageItem(ACCESS_TOKEN_KEY)
return getLocalStorageItem(getSessionKey(address))
}

export const removeAccessToken = (
address: string | undefined = getCurrentAccount()
) => {
if (!address) {
return null
return
}
const ACCESS_TOKEN_KEY = getSessionKey(address)
deleteLocalStorageItem(ACCESS_TOKEN_KEY)
deleteLocalStorageItem(getSessionKey(address))
}

export const saveAccessToken = ({
walletAddress,
accessToken,
}: Record<'walletAddress' | 'accessToken', string>) => {
const ACCESS_TOKEN_KEY = getSessionKey(walletAddress)
setLocalStorageItem(ACCESS_TOKEN_KEY, accessToken)
setLocalStorageItem(getSessionKey(walletAddress), accessToken)
return accessToken
}
Loading