-
-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(react): react SDK with context provider
- Loading branch information
1 parent
ca4ad2c
commit 1be502d
Showing
9 changed files
with
542 additions
and
76 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import LogtoClient from '@logto/browser'; | ||
import { createContext } from 'react'; | ||
|
||
export type LogtoContextProps = { | ||
logtoClient?: LogtoClient; | ||
loadingCount: number; | ||
setLoadingCount: React.Dispatch<React.SetStateAction<number>>; | ||
}; | ||
|
||
export const LogtoContext = createContext<LogtoContextProps>({ | ||
logtoClient: undefined, | ||
loadingCount: 0, | ||
setLoadingCount: (): never => { | ||
throw new Error('Must be used inside <LogtoProvider>'); | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import LogtoClient from '@logto/browser'; | ||
import { renderHook, act } from '@testing-library/react-hooks'; | ||
import React, { ComponentType } from 'react'; | ||
|
||
import useLogto from '.'; | ||
import { LogtoProvider } from '../provider'; | ||
|
||
const isSignInRedirected = jest.fn(() => false); | ||
const handleSignInCallback = jest.fn(async () => Promise.resolve()); | ||
|
||
jest.mock('@logto/browser', () => { | ||
return jest.fn().mockImplementation(() => { | ||
return { | ||
isSignInRedirected, | ||
handleSignInCallback, | ||
signIn: jest.fn(async () => Promise.resolve()), | ||
signOut: jest.fn(async () => Promise.resolve()), | ||
}; | ||
}); | ||
}); | ||
|
||
const endpoint = 'https://logto.dev'; | ||
const clientId = 'foo'; | ||
|
||
const createHookWrapper = | ||
() => | ||
({ children }: { children: ComponentType<unknown> }) => | ||
<LogtoProvider logtoConfig={{ endpoint, clientId }}>{children}</LogtoProvider>; | ||
|
||
describe('useLogto', () => { | ||
test('without provider should throw', () => { | ||
const { result } = renderHook(() => useLogto()); | ||
expect(result.error).not.toBeUndefined(); | ||
}); | ||
|
||
test('useLogto should call LogtoClient constructor', () => { | ||
renderHook(() => useLogto(), { | ||
wrapper: createHookWrapper(), | ||
}); | ||
|
||
expect(LogtoClient).toHaveBeenCalledWith({ endpoint, clientId }); | ||
}); | ||
|
||
test('useLogto should return LogtoClient property methods', async () => { | ||
const { result, waitFor } = renderHook(() => useLogto(), { | ||
wrapper: createHookWrapper(), | ||
}); | ||
|
||
await waitFor(() => { | ||
const { signIn, signOut, handleSignInCallback } = result.current; | ||
|
||
expect(signIn).not.toBeUndefined(); | ||
expect(signOut).not.toBeUndefined(); | ||
expect(handleSignInCallback).not.toBeUndefined(); | ||
}); | ||
}); | ||
|
||
test('not in callback url should not call `handleSignInCallback`', async () => { | ||
const { result } = renderHook(() => useLogto(), { | ||
wrapper: createHookWrapper(), | ||
}); | ||
await act(async () => result.current.signIn('redirectUri')); | ||
expect(handleSignInCallback).not.toHaveBeenCalled(); | ||
}); | ||
|
||
test('in callback url should call `handleSignInCallback`', async () => { | ||
isSignInRedirected.mockImplementation(() => true); | ||
const { result, waitFor } = renderHook(() => useLogto(), { | ||
wrapper: createHookWrapper(), | ||
}); | ||
await act(async () => result.current.signIn('redirectUri')); | ||
await waitFor(() => { | ||
expect(handleSignInCallback).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
import { IdTokenClaims, UserInfoResponse } from '@logto/browser'; | ||
import { Nullable } from '@silverhand/essentials'; | ||
import { useCallback, useContext, useEffect, useState } from 'react'; | ||
|
||
import { LogtoContext } from '../context'; | ||
|
||
type Logto = { | ||
isAuthenticated: boolean; | ||
isLoading: boolean; | ||
fetchUserInfo: () => Promise<UserInfoResponse>; | ||
getAccessToken: (resource?: string) => Promise<Nullable<string>>; | ||
getIdTokenClaims: () => IdTokenClaims; | ||
handleSignInCallback: (callbackUri: string) => Promise<void>; | ||
signIn: (redirectUri: string) => Promise<void>; | ||
signOut: (postLogoutRedirectUri: string) => Promise<void>; | ||
}; | ||
|
||
const notInProviderErrorMessage = 'useLogto hook must be used inside LogtoProvider context'; | ||
|
||
export default function useLogto(): Logto { | ||
const { logtoClient, loadingCount, setLoadingCount } = useContext(LogtoContext); | ||
const [isAuthenticated, setIsAuthenticated] = useState(logtoClient?.isAuthenticated ?? false); | ||
const isLoading = loadingCount > 0; | ||
|
||
const setLoadingState = useCallback( | ||
(state: boolean) => { | ||
if (state) { | ||
setLoadingCount((count) => count + 1); | ||
} else { | ||
setLoadingCount((count) => Math.max(0, count - 1)); | ||
} | ||
}, | ||
[setLoadingCount] | ||
); | ||
|
||
const signIn = useCallback( | ||
async (redirectUri: string) => { | ||
if (!logtoClient) { | ||
throw new Error(notInProviderErrorMessage); | ||
} | ||
|
||
setLoadingState(true); | ||
await logtoClient.signIn(redirectUri); | ||
setLoadingState(false); | ||
}, | ||
[logtoClient, setLoadingState] | ||
); | ||
|
||
const handleSignInCallback = useCallback( | ||
async (callbackUri: string) => { | ||
if (!logtoClient) { | ||
throw new Error(notInProviderErrorMessage); | ||
} | ||
setLoadingState(true); | ||
await logtoClient.handleSignInCallback(callbackUri); | ||
setLoadingState(false); | ||
setIsAuthenticated(true); | ||
}, | ||
[logtoClient, setLoadingState] | ||
); | ||
|
||
const signOut = useCallback( | ||
async (postLogoutRedirectUri: string) => { | ||
if (!logtoClient) { | ||
throw new Error(notInProviderErrorMessage); | ||
} | ||
setLoadingState(true); | ||
await logtoClient.signOut(postLogoutRedirectUri); | ||
setLoadingState(false); | ||
setIsAuthenticated(false); | ||
}, | ||
[logtoClient, setLoadingState] | ||
); | ||
|
||
const fetchUserInfo = useCallback(async () => { | ||
if (!logtoClient) { | ||
throw new Error(notInProviderErrorMessage); | ||
} | ||
setLoadingState(true); | ||
const userInfo = await logtoClient.fetchUserInfo(); | ||
setLoadingState(false); | ||
|
||
return userInfo; | ||
}, [logtoClient, setLoadingState]); | ||
|
||
const getAccessToken = useCallback(async () => { | ||
if (!logtoClient) { | ||
throw new Error(notInProviderErrorMessage); | ||
} | ||
setLoadingState(true); | ||
const accessToken = await logtoClient.getAccessToken(); | ||
setLoadingState(false); | ||
|
||
return accessToken; | ||
}, [logtoClient, setLoadingState]); | ||
|
||
const getIdTokenClaims = useCallback(() => { | ||
if (!logtoClient) { | ||
throw new Error(notInProviderErrorMessage); | ||
} | ||
|
||
return logtoClient.getIdTokenClaims(); | ||
}, [logtoClient]); | ||
|
||
useEffect(() => { | ||
if (!isAuthenticated && logtoClient?.isSignInRedirected(window.location.href)) { | ||
void handleSignInCallback(window.location.href); | ||
} | ||
}, [handleSignInCallback, isAuthenticated, logtoClient]); | ||
|
||
if (!logtoClient) { | ||
throw new Error(notInProviderErrorMessage); | ||
} | ||
|
||
return { | ||
isAuthenticated, | ||
isLoading, | ||
signIn, | ||
handleSignInCallback, | ||
signOut, | ||
fetchUserInfo, | ||
getAccessToken, | ||
getIdTokenClaims, | ||
}; | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,4 @@ | ||
/* istanbul ignore file */ | ||
export type TODO = 'TODO'; | ||
export type { LogtoContextProps } from './context'; | ||
export type { IdTokenClaims, UserInfoResponse } from '@logto/browser'; | ||
export * from './provider'; | ||
export { default as useLogto } from './hooks'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import LogtoClient, { LogtoConfig } from '@logto/browser'; | ||
import React, { useMemo, useState } from 'react'; | ||
|
||
import { LogtoContext } from './context'; | ||
|
||
export type LogtoProviderProps = { | ||
logtoConfig: LogtoConfig; | ||
children?: React.ReactNode; | ||
}; | ||
|
||
export const LogtoProvider = ({ logtoConfig, children }: LogtoProviderProps) => { | ||
const [loadingCount, setLoadingCount] = useState(0); | ||
const memorizedLogtoClient = useMemo( | ||
() => ({ logtoClient: new LogtoClient(logtoConfig) }), | ||
[logtoConfig] | ||
); | ||
const memorizedContextValue = useMemo( | ||
() => ({ | ||
...memorizedLogtoClient, | ||
loadingCount, | ||
setLoadingCount, | ||
}), | ||
[memorizedLogtoClient, loadingCount] | ||
); | ||
|
||
return <LogtoContext.Provider value={memorizedContextValue}>{children}</LogtoContext.Provider>; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,5 +5,6 @@ | |
], | ||
"exclude": [ | ||
"**/*.test.ts", | ||
"**/*.test.tsx", | ||
] | ||
} |
Oops, something went wrong.