diff --git a/src/components/MainList.test.tsx b/src/components/MainList.test.tsx index bbd5d93..50acbde 100644 --- a/src/components/MainList.test.tsx +++ b/src/components/MainList.test.tsx @@ -1,12 +1,11 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import { renderWithProviders } from '../utils/test-utils'; import MainList from './MainList'; type onClick = (event: React.MouseEvent) => void; describe('MainList', () => { let disableLogging: onClick; let enableLogging: onClick; - let loggedIn: boolean; let loggingEnabled: boolean; let logoutUser: onClick; let totalTimeLoggedToday: string; @@ -14,17 +13,15 @@ describe('MainList', () => { disableLogging = jest.fn(); enableLogging = jest.fn(); loggingEnabled = false; - loggedIn = false; logoutUser = jest.fn(); totalTimeLoggedToday = '1/1/1999'; }); it('should render properly', () => { - const { container } = render( + const { container } = renderWithProviders( , diff --git a/src/components/MainList.tsx b/src/components/MainList.tsx index 87886e3..874b712 100644 --- a/src/components/MainList.tsx +++ b/src/components/MainList.tsx @@ -1,9 +1,11 @@ import React from 'react'; +import { useSelector } from 'react-redux'; +import { ReduxSelector } from '../types/store'; +import { User } from '../types/user'; export interface MainListProps { disableLogging: (event: React.MouseEvent) => void; enableLogging: (event: React.MouseEvent) => void; - loggedIn: boolean; loggingEnabled: boolean; logoutUser: (event: React.MouseEvent) => void; totalTimeLoggedToday?: string; @@ -15,14 +17,17 @@ const openOptionsPage = async (): Promise => { export default function MainList({ disableLogging, enableLogging, - loggedIn, loggingEnabled, logoutUser, totalTimeLoggedToday, }: MainListProps): JSX.Element { + const user: User | undefined = useSelector( + (selector: ReduxSelector) => selector.currentUser.user, + ); + return (
- {loggedIn && ( + {user && (
@@ -34,7 +39,7 @@ export default function MainList({
)} - {loggingEnabled && loggedIn && ( + {loggingEnabled && user && (

@@ -45,7 +50,7 @@ export default function MainList({

)} - {!loggingEnabled && loggedIn && ( + {!loggingEnabled && user && (

@@ -61,7 +66,7 @@ export default function MainList({ Options - {loggedIn && ( + {user && (

)} - {!loggedIn && ( + {!user && ( selector.apiKey.value); - - const fetchUserData = async (): Promise => { - // await browser.storage.sync.set({ apiKey: API_KEY }); - let apiKey = ''; - if (!apiKeyFromRedux) { - const storage = await browser.storage.sync.get({ - apiKey: config.apiKey, - }); - apiKey = storage.apiKey as string; - dispatch(setValue(apiKey)); - } - - if (!apiKey) { - await changeExtensionState('notSignedIn'); - } - - try { - const data = await WakaTimeCore.checkAuth(apiKey); - dispatch(setUser(data)); - const items = await browser.storage.sync.get({ loggingEnabled: config.loggingEnabled }); - - if (items.loggingEnabled === true) { - await changeExtensionState('allGood'); - } else { - await changeExtensionState('notLogging'); - } - - const totalTimeLoggedToday = await WakaTimeCore.getTotalTimeLoggedToday(apiKey); - setState({ - ...state, - loggedIn: true, - loggingEnabled: items.loggingEnabled as boolean, - totalTimeLoggedToday: totalTimeLoggedToday.text, - }); - - await WakaTimeCore.recordHeartbeat(); - } catch (err: unknown) { - await changeExtensionState('notSignedIn'); - } - }; + const { + apiKey: apiKeyFromRedux, + loggingEnabled, + totalTimeLoggedToday, + }: ApiKeyReducer = useSelector((selector: ReduxSelector) => selector.config); useEffect(() => { - fetchUserData(); + fetchUserData(apiKeyFromRedux, dispatch); }, []); const disableLogging = async () => { @@ -109,10 +68,9 @@ export default function WakaTime(): JSX.Element {
diff --git a/src/reducers/apiKey.ts b/src/reducers/apiKey.ts deleted file mode 100644 index 442d20d..0000000 --- a/src/reducers/apiKey.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { createSlice } from '@reduxjs/toolkit'; - -interface setValueAction { - payload: string; - type: string; -} - -export interface ApiKey { - value: string; -} -export const initialState: ApiKey = { value: '' }; - -const apiKeySlice = createSlice({ - initialState, - name: 'spiKey', - reducers: { - setValue: (state, action: setValueAction) => { - state.value = action.payload; - }, - }, -}); - -export const actions = apiKeySlice.actions; -export const { setValue } = apiKeySlice.actions; -export default apiKeySlice.reducer; diff --git a/src/reducers/configReducer.ts b/src/reducers/configReducer.ts new file mode 100644 index 0000000..8567d2c --- /dev/null +++ b/src/reducers/configReducer.ts @@ -0,0 +1,50 @@ +import { createSlice } from '@reduxjs/toolkit'; +import config from '../config/config'; +import { ApiKeyReducer } from '../types/store'; + +interface SetApiKeyAction { + payload: string; + type: string; +} + +interface SetLoggingEnabledAction { + payload: boolean; + type: string; +} + +interface SetTotalTimeLoggedTodayAction { + payload: string; + type: string; +} + +export const initialConfigState: ApiKeyReducer = { + apiKey: '', + loggingEnabled: config.loggingEnabled, + totalTimeLoggedToday: '0 minutes', +}; + +const apiKeySlice = createSlice({ + initialState: initialConfigState, + name: 'configReducer', + reducers: { + configLogout: (state) => { + state.apiKey = ''; + state.loggingEnabled = config.loggingEnabled; + state.totalTimeLoggedToday = '0 minutes'; + }, + setApiKey: (state, action: SetApiKeyAction) => { + state.apiKey = action.payload; + }, + setLoggingEnabled: (state, action: SetLoggingEnabledAction) => { + state.loggingEnabled = action.payload; + }, + setTotalTimeLoggedToday: (state, action: SetTotalTimeLoggedTodayAction) => { + state.totalTimeLoggedToday = action.payload; + }, + }, +}); + +export const actions = apiKeySlice.actions; +export const { configLogout, setApiKey, setLoggingEnabled, setTotalTimeLoggedToday } = + apiKeySlice.actions; +export default apiKeySlice.reducer; diff --git a/src/reducers/currentUser.ts b/src/reducers/currentUser.ts index e5bb3c4..86fc627 100644 --- a/src/reducers/currentUser.ts +++ b/src/reducers/currentUser.ts @@ -39,9 +39,12 @@ const currentUser = createSlice({ setUser: (state, action: setUserAction) => { state.user = action.payload; }, + userLogout: (state) => { + state.user = undefined; + }, }, }); export const actions = currentUser.actions; -export const { setUser } = currentUser.actions; +export const { setUser, userLogout } = currentUser.actions; export default currentUser.reducer; diff --git a/src/stores/createStore.ts b/src/stores/createStore.ts index 5825ddc..770da04 100644 --- a/src/stores/createStore.ts +++ b/src/stores/createStore.ts @@ -1,22 +1,25 @@ -import { configureStore, Store } from '@reduxjs/toolkit'; +import { configureStore, Store, combineReducers } from '@reduxjs/toolkit'; import { logger } from 'redux-logger'; import { reduxBatch } from '@manaflair/redux-batch'; import devToolsEnhancer from 'remote-redux-devtools'; import currentUserReducer, { initialState as InitalCurrentUser } from '../reducers/currentUser'; -import apiKeyReducer from '../reducers/apiKey'; +import configReducer, { initialConfigState } from '../reducers/configReducer'; import isProd from '../utils/isProd'; -import { CurrentUser } from '../types/user'; -export interface RootState { - currentUser: CurrentUser; -} +// Create the root reducer separately so we can extract the RootState type +const rootReducer = combineReducers({ + config: configReducer, + currentUser: currentUserReducer, +}); + +export type RootState = ReturnType; const preloadedState: RootState = { + config: initialConfigState, currentUser: InitalCurrentUser, }; -export type RootStore = Store; -export default (appName: string): RootStore => { +export default (appName: string): Store => { const enhancers = []; enhancers.push(reduxBatch); if (!isProd()) { @@ -29,10 +32,7 @@ export default (appName: string): RootStore => { enhancers, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger), preloadedState, - reducer: { - apiKey: apiKeyReducer, - currentUser: currentUserReducer, - }, + reducer: rootReducer, }); return store; diff --git a/src/types/store.ts b/src/types/store.ts index a472505..64c8886 100644 --- a/src/types/store.ts +++ b/src/types/store.ts @@ -1,10 +1,12 @@ import { CurrentUser } from './user'; export interface ApiKeyReducer { - value: string; + apiKey: string; + loggingEnabled: boolean; + totalTimeLoggedToday: string; } export interface ReduxSelector { - apiKey: ApiKeyReducer; + config: ApiKeyReducer; currentUser: CurrentUser; } diff --git a/src/utils/checkCurrentUser.ts b/src/utils/checkCurrentUser.ts index c2a06a1..186c1f9 100644 --- a/src/utils/checkCurrentUser.ts +++ b/src/utils/checkCurrentUser.ts @@ -1,12 +1,13 @@ -import { RootStore } from '../stores/createStore'; +import { Store } from '@reduxjs/toolkit'; +import { RootState } from '../stores/createStore'; import { fetchCurrentUser } from '../reducers/currentUser'; import { ReduxSelector } from '../types/store'; type unsub = () => void; -export default (store: RootStore) => +export default (store: Store) => (time: number): unsub => { const fetchUser = () => { - const apiKey: string = (store.getState() as ReduxSelector).apiKey.value; + const apiKey: string = (store.getState() as ReduxSelector).config.apiKey; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error store.dispatch(fetchCurrentUser(apiKey)); diff --git a/src/utils/test-utils.tsx b/src/utils/test-utils.tsx new file mode 100644 index 0000000..d472da3 --- /dev/null +++ b/src/utils/test-utils.tsx @@ -0,0 +1,43 @@ +import React, { PropsWithChildren } from 'react'; +import { render } from '@testing-library/react'; +import type { RenderOptions } from '@testing-library/react'; +import { combineReducers, configureStore, Store } from '@reduxjs/toolkit'; +import type { PreloadedState } from '@reduxjs/toolkit'; +import { Provider } from 'react-redux'; +import { RootState } from '../stores/createStore'; + +// As a basic setup, import your same slice reducers +import userReducer, { initialState as InitalCurrentUser } from '../reducers/currentUser'; +import configReducer, { initialConfigState } from '../reducers/configReducer'; + +// This type interface extends the default options for render from RTL, as well +// as allows the user to specify other things such as initialState, store. +interface ExtendedRenderOptions extends Omit { + preloadedState?: PreloadedState; + store?: Store; +} + +const rootReducer = combineReducers({ + config: configReducer, + currentUser: userReducer, +}); + +export function renderWithProviders( + ui: React.ReactElement, + { + preloadedState = { + config: initialConfigState, + currentUser: InitalCurrentUser, + }, + // Automatically create a store instance if no store was passed in + store = configureStore({ preloadedState, reducer: rootReducer }), + ...renderOptions + }: ExtendedRenderOptions = {}, +): any { + function Wrapper({ children }: PropsWithChildren>): JSX.Element { + return {children}; + } + + // Return an object with the store and all of RTL's query functions + return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) }; +} diff --git a/src/utils/user.ts b/src/utils/user.ts new file mode 100644 index 0000000..d87b741 --- /dev/null +++ b/src/utils/user.ts @@ -0,0 +1,45 @@ +import { AnyAction, Dispatch } from '@reduxjs/toolkit'; +import { setApiKey, setLoggingEnabled, setTotalTimeLoggedToday } from '../reducers/configReducer'; +import config from '../config/config'; +import WakaTimeCore from '../core/WakaTimeCore'; +import { setUser } from '../reducers/currentUser'; +import changeExtensionState from './changeExtensionState'; + +export const fetchUserData = async ( + apiKey: string, + dispatch: Dispatch, +): Promise => { + if (!apiKey) { + const storage = await browser.storage.sync.get({ + apiKey: config.apiKey, + }); + apiKey = storage.apiKey as string; + dispatch(setApiKey(apiKey)); + } + + if (!apiKey) { + await changeExtensionState('notSignedIn'); + } + + try { + const [data, totalTimeLoggedTodayResponse, items] = await Promise.all([ + WakaTimeCore.checkAuth(apiKey), + WakaTimeCore.getTotalTimeLoggedToday(apiKey), + browser.storage.sync.get({ loggingEnabled: config.loggingEnabled }), + ]); + dispatch(setUser(data)); + + if (items.loggingEnabled === true) { + await changeExtensionState('allGood'); + } else { + await changeExtensionState('notLogging'); + } + + dispatch(setLoggingEnabled(items.loggingEnabled as boolean)); + dispatch(setTotalTimeLoggedToday(totalTimeLoggedTodayResponse.text)); + + await WakaTimeCore.recordHeartbeat(); + } catch (err: unknown) { + await changeExtensionState('notSignedIn'); + } +};