feat: persist userId

This commit is contained in:
Jeremy Kahn 2022-08-20 16:52:31 -05:00
parent 74f11dae2a
commit b8f8bb5bfd
6 changed files with 161 additions and 24 deletions

35
package-lock.json generated
View File

@ -21,6 +21,7 @@
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"fast-memoize": "^2.5.2",
"localforage": "^1.10.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^8.0.3",
@ -15987,6 +15988,22 @@
"node": ">=8.9.0"
}
},
"node_modules/localforage": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
"integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
"dependencies": {
"lie": "3.1.1"
}
},
"node_modules/localforage/node_modules/lie": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
"integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@ -35404,6 +35421,24 @@
"json5": "^2.1.2"
}
},
"localforage": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
"integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
"requires": {
"lie": "3.1.1"
},
"dependencies": {
"lie": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
"integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==",
"requires": {
"immediate": "~3.0.5"
}
}
}
},
"locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",

View File

@ -17,6 +17,7 @@
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"fast-memoize": "^2.5.2",
"localforage": "^1.10.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^8.0.3",

View File

@ -1,15 +1,70 @@
import React from 'react'
import { render } from '@testing-library/react'
import { act, render } from '@testing-library/react'
import { MemoryRouter as Router } from 'react-router-dom'
import localforage from 'localforage'
import Bootstrap from './Bootstrap'
import { PersistedStorageKeys } from 'models/storage'
const StubBootstrap = () => (
import Bootstrap, { BootstrapProps } from './Bootstrap'
const mockPersistedStorage =
jest.createMockFromModule<jest.Mock<typeof localforage>>('localforage')
const mockGetUuid = jest.fn()
const mockGetItem = jest.fn()
const mockSetItem = jest.fn()
beforeEach(() => {
mockGetItem.mockImplementation(() => Promise.resolve(null))
mockSetItem.mockImplementation((data: any) => Promise.resolve(data))
})
const renderBootstrap = async (overrides: BootstrapProps = {}) => {
Object.assign(mockPersistedStorage, {
getItem: mockGetItem,
setItem: mockSetItem,
})
render(
<Router>
<Bootstrap />
<Bootstrap
persistedStorage={mockPersistedStorage as any as typeof localforage}
{...overrides}
/>
</Router>
)
test('renders', () => {
render(<StubBootstrap />)
// https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning#an-alternative-waiting-for-the-mocked-promise
await act(async () => {
await Promise.resolve()
})
}
test('renders', async () => {
await renderBootstrap()
})
test('checks persistedStorage for user settings', async () => {
await renderBootstrap()
expect(mockGetItem).toHaveBeenCalledWith(PersistedStorageKeys.USER_SETTINGS)
})
test('persists user settings if none were already persisted', async () => {
await renderBootstrap({
getUuid: mockGetUuid.mockImplementation(() => 'abc123'),
})
expect(mockSetItem).toHaveBeenCalledWith(PersistedStorageKeys.USER_SETTINGS, {
userId: 'abc123',
})
})
test('does not update user settings if they were already persisted', async () => {
mockGetItem.mockImplementation(() => ({
userId: 'abc123',
}))
await renderBootstrap()
expect(mockSetItem).not.toHaveBeenCalled()
})

View File

@ -1,15 +1,54 @@
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { Routes, Route } from 'react-router-dom'
import { v4 as uuid } from 'uuid'
import localforage from 'localforage'
import { Home } from './pages/Home/'
import { PublicRoom } from './pages/PublicRoom/'
import { Home } from 'pages/Home/'
import { PublicRoom } from 'pages/PublicRoom/'
import { UserSettings } from 'models/settings'
import { PersistedStorageKeys } from 'models/storage'
function Bootstrap() {
const [userId] = useState(uuid())
export interface BootstrapProps {
persistedStorage?: typeof localforage
getUuid?: typeof uuid
}
function Bootstrap({
persistedStorage = localforage.createInstance({
name: 'chitchatter',
description: 'Persisted settings data for chitchatter',
}),
getUuid = uuid,
}: BootstrapProps) {
const [hasLoadedSettings, setHasLoadedSettings] = useState(false)
const [settings, setSettings] = useState({ userId: getUuid() })
const { userId } = settings
useEffect(() => {
;(async () => {
if (hasLoadedSettings) return
const persistedUserSettings =
await persistedStorage.getItem<UserSettings>(
PersistedStorageKeys.USER_SETTINGS
)
if (persistedUserSettings) {
setSettings(persistedUserSettings)
} else {
await persistedStorage.setItem(
PersistedStorageKeys.USER_SETTINGS,
settings
)
}
setHasLoadedSettings(true)
})()
}, [hasLoadedSettings, persistedStorage, settings, userId])
return (
<div className="Chitchatter">
{hasLoadedSettings ? (
<Routes>
{['/', '/index.html'].map(path => (
<Route key={path} path={path} element={<Home />} />
@ -19,6 +58,7 @@ function Bootstrap() {
element={<PublicRoom userId={userId} />}
/>
</Routes>
) : null}
</div>
)
}

3
src/models/settings.ts Normal file
View File

@ -0,0 +1,3 @@
export interface UserSettings {
userId: string
}

3
src/models/storage.ts Normal file
View File

@ -0,0 +1,3 @@
export enum PersistedStorageKeys {
USER_SETTINGS = 'userSettings',
}