-
Notifications
You must be signed in to change notification settings - Fork 305
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
feat(A.Next): add useQRCodeString for RN #4886
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { Authenticator } from './Authenticator'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { useQRCodeString } from './useQRCodeString'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`useQRCodeString behaves as expected in the happy path 1`] = ` | ||
" | ||
|
||
█▀▀▀▀▀█ ▀██▀▀ █▀▀▀▀▀█ | ||
█ ███ █ ▄▀▀█▀ █ ███ █ | ||
█ ▀▀▀ █ █ ▄▀ █ ▀▀▀ █ | ||
▀▀▀▀▀▀▀ █ ▀▄█ ▀▀▀▀▀▀▀ | ||
▀▄ █ ▀▀█▄██▄▀▀█▀█▄▄▀ | ||
▀ ▀ █▀▀ ██▀▀ ▄█▀█ ▀ | ||
▀ ▀ ▀▀▄▄█ ███ ▀▄ ▀ | ||
█▀▀▀▀▀█ ▀▄▀▄█▀ ▄ ▀█ | ||
█ ███ █ ▀▄ █▄ ▀▀▀█▄▄█ | ||
█ ▀▀▀ █ ▄█▀ ▄█▀█▀ | ||
▀▀▀▀▀▀▀ ▀ ▀ ▀▀▀ ▀▀ ▀ | ||
|
||
" | ||
`; | ||
|
||
exports[`useQRCodeString does not call \`onSuccess\` if it is not a function 1`] = ` | ||
" | ||
|
||
█▀▀▀▀▀█ ▀██▀▀ █▀▀▀▀▀█ | ||
reesscot marked this conversation as resolved.
Show resolved
Hide resolved
|
||
█ ███ █ ▄▀▀█▀ █ ███ █ | ||
█ ▀▀▀ █ █ ▄▀ █ ▀▀▀ █ | ||
▀▀▀▀▀▀▀ █ ▀▄█ ▀▀▀▀▀▀▀ | ||
▀▄ █ ▀▀█▄██▄▀▀█▀█▄▄▀ | ||
▀ ▀ █▀▀ ██▀▀ ▄█▀█ ▀ | ||
▀ ▀ ▀▀▄▄█ ███ ▀▄ ▀ | ||
█▀▀▀▀▀█ ▀▄▀▄█▀ ▄ ▀█ | ||
█ ███ █ ▀▄ █▄ ▀▀▀█▄▄█ | ||
█ ▀▀▀ █ ▄█▀ ▄█▀█▀ | ||
▀▀▀▀▀▀▀ ▀ ▀ ▀▀▀ ▀▀ ▀ | ||
|
||
" | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
import { renderHook } from '@testing-library/react-hooks'; | ||
import QRCodeModule from 'qrcode'; | ||
|
||
import { useQRCodeString, UseQRCodeStringParams } from '../useQRCodeString'; | ||
|
||
const toStringSpy = jest.spyOn(QRCodeModule, 'toString'); | ||
const TEST_STRING = 'testy'; | ||
|
||
const onError = jest.fn(); | ||
const onSuccess = jest.fn(); | ||
|
||
const BASE_INPUT = { text: TEST_STRING, onError, onSuccess }; | ||
const INVALID_INPUT = { | ||
text: TEST_STRING, | ||
onError: 'not a function', | ||
onSuccess: 'also not a function', | ||
}; | ||
|
||
describe('useQRCodeString', () => { | ||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('behaves as expected in the happy path', async () => { | ||
const { waitForNextUpdate, result } = renderHook( | ||
(input: UseQRCodeStringParams = BASE_INPUT) => useQRCodeString(input) | ||
); | ||
|
||
expect(result.current.isLoading).toBe(true); | ||
expect(result.current.hasError).toBe(false); | ||
expect(result.current.qrCodeString).toBeNull(); | ||
|
||
await waitForNextUpdate(); | ||
|
||
expect(result.current.isLoading).toBe(false); | ||
expect(result.current.hasError).toBe(false); | ||
expect(result.current.qrCodeString).toMatchSnapshot(); | ||
|
||
expect(onSuccess).toHaveBeenCalledTimes(1); | ||
expect(onSuccess).toHaveBeenCalledWith(result.current.qrCodeString); | ||
}); | ||
|
||
it('behaves as expected when `toString` throws an error', async () => { | ||
const error = new Error('Rejected!'); | ||
|
||
(toStringSpy as jest.Mock).mockRejectedValueOnce(error); | ||
|
||
const { waitForNextUpdate, result } = renderHook(() => | ||
useQRCodeString(BASE_INPUT) | ||
); | ||
|
||
expect(result.current.isLoading).toBe(true); | ||
expect(result.current.hasError).toBe(false); | ||
expect(result.current.qrCodeString).toBeNull(); | ||
|
||
await waitForNextUpdate(); | ||
|
||
expect(result.current.isLoading).toBe(false); | ||
expect(result.current.hasError).toBe(true); | ||
expect(result.current.qrCodeString).toBeNull(); | ||
|
||
expect(onError).toHaveBeenCalledTimes(1); | ||
expect(onError).toHaveBeenCalledWith(error.message); | ||
}); | ||
|
||
it('does not call `toString` when `text` param is not provided', () => { | ||
const { result } = renderHook(useQRCodeString); | ||
expect(result.current.isLoading).toBe(false); | ||
expect(result.current.hasError).toBe(false); | ||
expect(result.current.qrCodeString).toBeNull(); | ||
}); | ||
|
||
it('ignores the first response if rerun a second time before the first call resolves in the happy path', async () => { | ||
const firstResponse = 'first response'; | ||
const secondResponse = 'second response'; | ||
|
||
(toStringSpy as jest.Mock) | ||
.mockResolvedValueOnce(firstResponse) | ||
.mockResolvedValueOnce(secondResponse); | ||
|
||
const { waitForNextUpdate, result, rerender } = renderHook( | ||
(input: UseQRCodeStringParams = BASE_INPUT) => useQRCodeString(input) | ||
); | ||
|
||
expect(result.current.isLoading).toBe(true); | ||
expect(result.current.hasError).toBe(false); | ||
expect(result.current.qrCodeString).toBeNull(); | ||
|
||
rerender({ ...BASE_INPUT, text: 'new value' }); | ||
|
||
expect(result.current.isLoading).toBe(true); | ||
expect(result.current.hasError).toBe(false); | ||
expect(result.current.qrCodeString).toBeNull(); | ||
|
||
await waitForNextUpdate(); | ||
|
||
expect(toStringSpy).toHaveBeenCalledTimes(2); | ||
|
||
expect(result.current.isLoading).toBe(false); | ||
expect(result.current.hasError).toBe(false); | ||
expect(result.current.qrCodeString).toBe(secondResponse); | ||
|
||
expect(onSuccess).toHaveBeenCalledTimes(1); | ||
expect(onSuccess).toHaveBeenCalledWith(secondResponse); | ||
}); | ||
|
||
it('ignores the first response if rerun a second time before the first call resolves in the unhappy path', async () => { | ||
const firstError = new Error('first response'); | ||
const secondError = new Error('second response'); | ||
|
||
(toStringSpy as jest.Mock) | ||
.mockRejectedValueOnce(firstError) | ||
.mockRejectedValueOnce(secondError); | ||
|
||
const { waitForNextUpdate, result, rerender } = renderHook( | ||
(input: UseQRCodeStringParams = BASE_INPUT) => useQRCodeString(input) | ||
); | ||
|
||
expect(result.current.isLoading).toBe(true); | ||
expect(result.current.hasError).toBe(false); | ||
expect(result.current.qrCodeString).toBeNull(); | ||
|
||
rerender({ ...BASE_INPUT, text: 'new value' }); | ||
|
||
expect(result.current.isLoading).toBe(true); | ||
expect(result.current.hasError).toBe(false); | ||
expect(result.current.qrCodeString).toBeNull(); | ||
|
||
await waitForNextUpdate(); | ||
|
||
expect(toStringSpy).toHaveBeenCalledTimes(2); | ||
|
||
expect(result.current.isLoading).toBe(false); | ||
expect(result.current.hasError).toBe(true); | ||
expect(result.current.qrCodeString).toBeNull(); | ||
|
||
expect(onError).toHaveBeenCalledTimes(1); | ||
expect(onError).toHaveBeenCalledWith(secondError.message); | ||
}); | ||
|
||
it('calls `toString` when `text` param changes', async () => { | ||
const { rerender, waitForNextUpdate } = renderHook( | ||
(input: UseQRCodeStringParams = BASE_INPUT) => useQRCodeString(input) | ||
); | ||
|
||
await waitForNextUpdate(); | ||
|
||
expect(toStringSpy).toHaveBeenCalledTimes(1); | ||
|
||
// rerender with same input | ||
rerender(BASE_INPUT); | ||
|
||
expect(toStringSpy).toHaveBeenCalledTimes(1); | ||
|
||
// rerender with new `text` param | ||
rerender({ ...BASE_INPUT, text: 'new value' }); | ||
|
||
await waitForNextUpdate(); | ||
|
||
expect(toStringSpy).toHaveBeenCalledTimes(2); | ||
}); | ||
|
||
it('does not call `onSuccess` if it is not a function', async () => { | ||
const { waitForNextUpdate, result } = renderHook( | ||
// @ts-expect-error test against invalid input | ||
() => useQRCodeString(INVALID_INPUT) | ||
); | ||
|
||
expect(result.current.isLoading).toBe(true); | ||
expect(result.current.hasError).toBe(false); | ||
expect(result.current.qrCodeString).toBeNull(); | ||
|
||
await waitForNextUpdate(); | ||
|
||
expect(result.current.isLoading).toBe(false); | ||
expect(result.current.hasError).toBe(false); | ||
expect(result.current.qrCodeString).toMatchSnapshot(); | ||
}); | ||
|
||
it('does not call `onError` if it is not a function', async () => { | ||
const error = new Error('Rejected!'); | ||
|
||
(toStringSpy as jest.Mock).mockRejectedValueOnce(error); | ||
|
||
const { waitForNextUpdate, result } = renderHook( | ||
// @ts-expect-error test against invalid input | ||
() => useQRCodeString(INVALID_INPUT) | ||
); | ||
|
||
expect(result.current.isLoading).toBe(true); | ||
expect(result.current.hasError).toBe(false); | ||
expect(result.current.qrCodeString).toBeNull(); | ||
|
||
await waitForNextUpdate(); | ||
|
||
expect(result.current.isLoading).toBe(false); | ||
expect(result.current.hasError).toBe(true); | ||
expect(result.current.qrCodeString).toBeNull(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { useQRCodeString } from './useQRCodeString'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import React from 'react'; | ||
import { toString as toQRCodeString, QRCodeToStringOptions } from 'qrcode'; | ||
|
||
import { isFunction } from '@aws-amplify/ui'; | ||
|
||
export type UseQRCodeStringParams = { | ||
onError?: (err: string) => void; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have a general pattern or philosophy on whether it should be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good question 😄 We do not, for internal hooks personally have been leaning towards |
||
onSuccess?: (output: string) => void; | ||
text?: string; | ||
options?: QRCodeToStringOptions; | ||
}; | ||
|
||
interface UseQRCodeString { | ||
hasError: boolean; | ||
isLoading: boolean; | ||
qrCodeString: string | null; | ||
} | ||
|
||
interface UseQRCodeState | ||
extends Pick<UseQRCodeString, 'hasError' | 'qrCodeString'> {} | ||
|
||
const INITIAL_OUTPUT: UseQRCodeState = { hasError: false, qrCodeString: null }; | ||
const ERROR_OUTPUT: UseQRCodeState = { hasError: true, qrCodeString: null }; | ||
|
||
/** | ||
* Generates a QR code string from provided `text` param | ||
* | ||
* @param {UseQRCodeStringParams} params target text and event callbacks | ||
* @returns {UseQRCodeString} QR code string and related values | ||
*/ | ||
export function useQRCodeString( | ||
params?: UseQRCodeStringParams | ||
): UseQRCodeString { | ||
const { onError, onSuccess, text, options } = params ?? {}; | ||
const [{ hasError, qrCodeString }, setOutput] = | ||
React.useState<UseQRCodeState>(() => INITIAL_OUTPUT); | ||
|
||
// only true when a `text` param has been provided and | ||
// both `qrCodeString` and `hasError` are falsy | ||
const isLoading = !!(text && !qrCodeString && !hasError); | ||
|
||
React.useEffect(() => { | ||
if (!text) { | ||
return; | ||
} | ||
let ignore = false; | ||
|
||
toQRCodeString(text, options) | ||
.then((_qrCodeString) => { | ||
if (ignore) { | ||
return; | ||
} | ||
|
||
if (isFunction(onSuccess)) { | ||
onSuccess(_qrCodeString); | ||
} | ||
setOutput({ hasError: false, qrCodeString: _qrCodeString }); | ||
}) | ||
.catch((error) => { | ||
if (ignore) { | ||
return; | ||
} | ||
|
||
if (isFunction(onError)) { | ||
onError((error as Error).message); | ||
} | ||
setOutput(ERROR_OUTPUT); | ||
}); | ||
|
||
return () => { | ||
ignore = true; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this because the hook can be unmounted before the async toQRCodeString completes? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep 😅 |
||
}; | ||
}, [onError, onSuccess, options, text]); | ||
|
||
return { hasError, isLoading, qrCodeString }; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
export { Authenticator } from './Authenticator'; | ||
export { Authenticator } from './components'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice!