Skip to content

Commit b735020

Browse files
BA-1390: sync cookie retrieval (#95)
1 parent 437afce commit b735020

31 files changed

+284
-68
lines changed

packages/authentication/CHANGELOG.md

+13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# @baseapp-frontend/authentication
22

3+
## 2.2.0
4+
5+
### Minor Changes
6+
7+
- The `useJWTUser` hook now uses `getUser` function to retrieve the user's data. The `noSSR` option is set to `false` by default, which allows cookies to be retrieved right away on the server, but also forces the Next.js page to be dynamically rendered.
8+
- The `getUser` is no longer an async function or returns awaits for the `getToken` return.
9+
- The `withUser` is no longer an async function or returns awaits for the `getUser` return.
10+
11+
### Patch Changes
12+
13+
- Updated dependencies
14+
- @baseapp-frontend/utils@2.3.0
15+
316
## 2.1.3
417

518
### Patch Changes

packages/authentication/modules/user/getUser/__tests__/getUser.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import jwt from './fixtures/jwt.json'
88
describe('getUser', () => {
99
it('should return the user from the JWT cookie', async () => {
1010
;(Cookies.get as CookiesGetByNameFn) = jest.fn(() => jwt.token)
11-
const user = await getUser()
11+
const user = getUser()
1212

1313
expect(user?.email).toBe('user@company.com')
1414
expect(user?.firstName).toBe('John')
@@ -17,7 +17,7 @@ describe('getUser', () => {
1717

1818
it('should return null if the JWT cookie is not set', async () => {
1919
;(Cookies.get as CookiesGetByNameFn) = jest.fn(() => undefined)
20-
const user = await getUser()
20+
const user = getUser()
2121

2222
expect(user).toBeNull()
2323
})

packages/authentication/modules/user/getUser/index.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import { IJWTContent } from '@baseapp-frontend/utils/types/jwt'
55
import { IUser } from '../../../types/user'
66
import { GetUserOptions } from './types'
77

8-
const getUser = async <TUser extends Partial<IUser> & IJWTContent>({
8+
const getUser = <TUser extends Partial<IUser> & IJWTContent>({
99
cookieName = ACCESS_COOKIE_NAME,
10+
noSSR = false,
1011
}: GetUserOptions = {}) => {
11-
const token = await getToken(cookieName)
12+
const token = getToken(cookieName, { noSSR })
1213
if (token) {
1314
try {
1415
const user = decodeJWT<TUser>(token)
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { ServerSideRenderingOption } from '@baseapp-frontend/utils'
2+
13
import { ICookieName } from '../../../types/auth'
24

3-
export interface GetUserOptions extends ICookieName {}
5+
export interface GetUserOptions extends ICookieName, ServerSideRenderingOption {}

packages/authentication/modules/user/useJWTUser/index.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
1-
import { ACCESS_COOKIE_NAME, decodeJWT } from '@baseapp-frontend/utils'
1+
import { ACCESS_COOKIE_NAME } from '@baseapp-frontend/utils'
2+
import { IJWTContent } from '@baseapp-frontend/utils/types/jwt'
23

34
import { useQuery, useQueryClient } from '@tanstack/react-query'
45
import Cookies from 'js-cookie'
56

67
import UserApi, { USER_API_KEY } from '../../../services/user'
78
import { IUser } from '../../../types/user'
9+
import getUser from '../getUser'
810
import { IUseJWTUser } from './types'
911

1012
/**
1113
* Fetches the user data using the JWT token data as placeholder data.
1214
*
1315
* This makes user data available before fetching it from the server.
16+
*
17+
* Be aware that, by using `useJWTUser` with `noSSR` set to `false`, will make the Next.js page to opt-out from Static Rendering and be dynamically rendered.
1418
*/
15-
const useJWTUser = <TUser extends Partial<IUser>>({
19+
const useJWTUser = <TUser extends Partial<IUser> & IJWTContent>({
1620
options,
1721
cookieName = ACCESS_COOKIE_NAME,
1822
ApiClass = UserApi,
23+
noSSR = false,
1924
}: IUseJWTUser<TUser> = {}) => {
20-
const token = Cookies.get(cookieName) || ''
21-
const placeholderData = decodeJWT<TUser>(token) || undefined
25+
const token = Cookies.get(cookieName) ?? ''
26+
const placeholderData = getUser<TUser>({ cookieName, noSSR })
2227
const queryClient = useQueryClient()
2328

2429
const { data: user, ...rest } = useQuery({
@@ -29,6 +34,7 @@ const useJWTUser = <TUser extends Partial<IUser>>({
2934
useErrorBoundary: false,
3035
placeholderData,
3136
...options, // needs to be placed bellow all overridable options
37+
// @ts-ignore TODO: not sure what went wrong here, but this onError shall be removed when we update react query
3238
onError: (error: any) => {
3339
if (error?.response?.status === 401) {
3440
// we don't need to remove cookies here since this should be done by the interceptor
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
import { ServerSideRenderingOption } from '@baseapp-frontend/utils'
2+
13
import { UseQueryOptions } from '@tanstack/react-query'
24

35
import UserApi from '../../../services/user'
46
import { ICookieName } from '../../../types/auth'
57

68
type ApiClass = Pick<typeof UserApi, 'getUser'>
79

8-
export interface IUseJWTUser<IUser> extends ICookieName {
10+
export interface IUseJWTUser<IUser> extends ICookieName, ServerSideRenderingOption {
911
options?: UseQueryOptions<IUser, unknown, IUser, any>
1012
ApiClass?: ApiClass
1113
}

packages/authentication/modules/user/withUser/__tests__/withUser.tst.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,20 @@ describe('withUser HOC', () => {
2121

2222
it('passes the user object to the wrapped component', async () => {
2323
const userMock = { firstName: 'John', lastName: 'Doe' }
24-
getUserMock.mockResolvedValue(userMock)
24+
getUserMock.mockReturnValue(userMock)
2525

2626
const WithUserComponent = withUser<User>(MockComponent)
27-
const { findByText } = render(await WithUserComponent({}))
27+
const { findByText } = render(WithUserComponent({}))
2828

2929
const userElement = await findByText(`Hello, ${userMock.firstName}`)
3030
expect(userElement).toBeInTheDocument()
3131
})
3232

3333
it('handles the case when there is no user', async () => {
34-
getUserMock.mockResolvedValue(null)
34+
getUserMock.mockReturnValue(null)
3535

3636
const WithUserComponent = withUser<User>(MockComponent)
37-
const { findByText } = render(await WithUserComponent({}))
37+
const { findByText } = render(WithUserComponent({}))
3838

3939
const userElement = await findByText('No user')
4040
expect(userElement).toBeInTheDocument()

packages/authentication/modules/user/withUser/index.tsx

+3-23
Original file line numberDiff line numberDiff line change
@@ -6,38 +6,18 @@ import getUser from '../getUser'
66
import { ComponentWithUser } from './types'
77

88
/**
9-
* HOC to provide the `user` object to the component.
10-
*
11-
* It's useful when the component that needs to call `getUser` and can't be async (e.g. Client Components).
9+
* HOC to provide the `user` object to the component as a prop.
1210
*
1311
* Consider rendering the Component using the `ComponentWithUser` interface to inherit the `user` type.
14-
* You may need to import the component using dynamic imports with `next/dynamic`.
15-
*
16-
* @example Defining and exporting a component with `withUser`:
17-
* ```tsx
18-
* import { FC } from 'react'
19-
*
20-
* import {ComponentWithUser, withUser} from '@baseapp/authentication'
21-
*
22-
* const MyComponent: FC<ComponentWithUser<MyUser>> = ({user}) => <div>Hi {user.firstName}</div>
23-
*
24-
* export default withUser<MyUser>(MyComponent)
25-
* ```
26-
* @example Importing a component with `withUser` using `next/dynamic`:
27-
* ```tsx
28-
* import dynamic from 'next/dynamic'
29-
*
30-
* const MyComponent = dynamic(() => import('./MyComponent'), { ssr: false })
3112
*
32-
* (...)
3313
* ```
3414
*/
3515
const withUser =
3616
<TUser extends IJWTContent, Props extends object = {}>(
3717
Component: FC<Props & ComponentWithUser<TUser>>,
3818
) =>
39-
async (props: Props) => {
40-
const user = await getUser<TUser>()
19+
(props: Props) => {
20+
const user = getUser<TUser>()
4121

4222
return <Component {...props} user={user} />
4323
}

packages/authentication/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@baseapp-frontend/authentication",
33
"description": "Authentication modules.",
4-
"version": "2.1.3",
4+
"version": "2.2.0",
55
"main": "./dist/index.ts",
66
"module": "./dist/index.mjs",
77
"scripts": {

packages/config/.eslintrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ module.exports = {
3434
],
3535
plugins: ['react', 'react-hooks', '@typescript-eslint', '@emotion'],
3636
rules: {
37+
'global-require': 0,
3738
'@emotion/jsx-import': 'error',
3839
'@emotion/no-vanilla': 'error',
3940
'@emotion/import-from-emotion': 'error',

packages/config/CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# @baseapp-frontend/config
22

3+
## 2.1.4
4+
5+
### Patch Changes
6+
7+
- Ignore `global-require` rule.
8+
39
## 2.1.3
410

511
### Patch Changes

packages/config/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@baseapp-frontend/config",
33
"description": "Reusable configurations for eslint, prettier, jest and relay",
4-
"version": "2.1.3",
4+
"version": "2.1.4",
55
"main": "index.js",
66
"files": [
77
".eslintrc.js",

packages/graphql/CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# @baseapp-frontend/graphql
22

3+
## 1.1.1
4+
5+
### Patch Changes
6+
7+
- `connectionParams` function on the `wsClient` is now sync and don't wait for the `getToken` response.
8+
- Updated dependencies
9+
- @baseapp-frontend/utils@2.3.0
10+
311
## 1.1.0
412

513
### Minor Changes

packages/graphql/config/environment.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ export async function httpFetch(
9292

9393
const wsClient = createClient({
9494
url: process.env.NEXT_PUBLIC_WS_RELAY_ENDPOINT as string,
95-
connectionParams: async () => {
96-
const Authorization = await getToken()
95+
connectionParams: () => {
96+
const Authorization = getToken()
9797
if (!Authorization) return {}
9898
return { Authorization }
9999
},

packages/graphql/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@baseapp-frontend/graphql",
33
"description": "GraphQL configurations and utilities",
4-
"version": "1.1.0",
4+
"version": "1.1.1",
55
"main": "./dist/index.ts",
66
"module": "./dist/index.mjs",
77
"scripts": {

packages/utils/CHANGELOG.md

+9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# @baseapp-frontend/utils
22

3+
## 2.3.0
4+
5+
### Minor Changes
6+
7+
- Add cookie management functions like `getCookie`. It can optionally enable cookies retrieval on the server side. By doing so, it will force the Next.js page to be dynamically rendered.
8+
- The `getToken` function now uses `getCookie` function and it no longer returns a promise nor needs to be waited/resolved.
9+
- The `baseAppFetch` function can now optionally `throw` api errors if `throwError` option is set to `true`. It is enabled by default.
10+
11+
312
## 2.2.3
413

514
### Patch Changes
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import ClientCookies from 'js-cookie'
2+
3+
import { getCookie, removeCookie, setCookie } from '..'
4+
5+
jest.mock('js-cookie', () => ({
6+
get: jest.fn(),
7+
set: jest.fn(),
8+
remove: jest.fn(),
9+
}))
10+
11+
describe('Cookie Functions in the client side', () => {
12+
describe('getCookie', () => {
13+
it('should get a cookie from the client', () => {
14+
const mockCookie = 'test-cookie'
15+
;(ClientCookies.get as jest.Mock).mockReturnValue(mockCookie)
16+
17+
const result = getCookie('test')
18+
expect(result).toBe(mockCookie)
19+
expect(ClientCookies.get).toHaveBeenCalledWith('test')
20+
})
21+
22+
it('should be able to parse the cookie if it is a JSON string', () => {
23+
const mockCookie = '{"key": "value"}'
24+
;(ClientCookies.get as jest.Mock).mockReturnValue(mockCookie)
25+
26+
const result = getCookie('test')
27+
28+
expect(result).toEqual({ key: 'value' })
29+
})
30+
31+
it('should return raw cookie if JSON parsing fails', () => {
32+
;(ClientCookies.get as jest.Mock).mockImplementation(() => '{"malformedJson":')
33+
34+
const result = getCookie('test')
35+
36+
expect(result).toBe('{"malformedJson":')
37+
})
38+
39+
it('should not parse the cookie if parseJSON is false', () => {
40+
const mockCookie = '{"key": "value"}'
41+
;(ClientCookies.get as jest.Mock).mockReturnValue(mockCookie)
42+
43+
const result = getCookie('test', { parseJSON: false })
44+
45+
expect(result).toBe(mockCookie)
46+
})
47+
})
48+
49+
describe('setCookie', () => {
50+
it('should set a cookie', () => {
51+
const key = 'test'
52+
const value = { data: 'test-data' }
53+
const config = { expires: 7 }
54+
55+
setCookie(key, value, config)
56+
expect(ClientCookies.set).toHaveBeenCalledWith(key, JSON.stringify(value), config)
57+
})
58+
59+
it('should handle errors gracefully', () => {
60+
;(ClientCookies.set as jest.Mock).mockImplementation(() => {
61+
throw new Error('error')
62+
})
63+
64+
expect(() => setCookie('test', 'value', {})).not.toThrow()
65+
})
66+
})
67+
68+
describe('removeCookie', () => {
69+
it('should remove a cookie', () => {
70+
const key = 'test'
71+
72+
removeCookie(key)
73+
expect(ClientCookies.remove).toHaveBeenCalledWith(key)
74+
})
75+
76+
it('should handle errors gracefully', () => {
77+
;(ClientCookies.remove as jest.Mock).mockImplementation(() => {
78+
throw new Error('error')
79+
})
80+
81+
expect(() => removeCookie('test')).not.toThrow()
82+
})
83+
})
84+
})

0 commit comments

Comments
 (0)