-
Notifications
You must be signed in to change notification settings - Fork 2
feat: add reCAPTCHA #93
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
Changes from all commits
6c4ae3c
785ee80
751d1a6
137635a
424b71d
68087b6
4110192
426b7ff
b218741
d6d0062
76e0de0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,8 +1,19 @@ | ||||||||||||||||||||||||||||||||||||
| import { getConfig } from '@edx/frontend-platform/config'; | ||||||||||||||||||||||||||||||||||||
| import { ReactElement } from 'react'; | ||||||||||||||||||||||||||||||||||||
| import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3'; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| import PlanDetailsPage from '@/components/plan-details-pages/PlanDetailsPage'; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // TODO: unnecessary layer of abstraction, just move component logic into this file. | ||||||||||||||||||||||||||||||||||||
| const PlanDetails: React.FC = () => ( | ||||||||||||||||||||||||||||||||||||
| <PlanDetailsPage /> | ||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||
| const PlanDetails = (): ReactElement => { | ||||||||||||||||||||||||||||||||||||
| const { RECAPTCHA_SITE_KEY_WEB } = getConfig(); | ||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||
| <GoogleReCaptchaProvider | ||||||||||||||||||||||||||||||||||||
| reCaptchaKey={RECAPTCHA_SITE_KEY_WEB} | ||||||||||||||||||||||||||||||||||||
| useEnterprise | ||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||
| <PlanDetailsPage /> | ||||||||||||||||||||||||||||||||||||
| </GoogleReCaptchaProvider> | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+9
to
+15
|
||||||||||||||||||||||||||||||||||||
| return ( | |
| <GoogleReCaptchaProvider | |
| reCaptchaKey={RECAPTCHA_SITE_KEY_WEB} | |
| useEnterprise | |
| > | |
| <PlanDetailsPage /> | |
| </GoogleReCaptchaProvider> | |
| const hasValidRecaptchaKey = typeof RECAPTCHA_SITE_KEY_WEB === 'string' && RECAPTCHA_SITE_KEY_WEB.trim() !== ''; | |
| return hasValidRecaptchaKey ? ( | |
| <GoogleReCaptchaProvider | |
| reCaptchaKey={RECAPTCHA_SITE_KEY_WEB} | |
| useEnterprise | |
| > | |
| <PlanDetailsPage /> | |
| </GoogleReCaptchaProvider> | |
| ) : ( | |
| <PlanDetailsPage /> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| import { getConfig } from '@edx/frontend-platform'; | ||
| import { logError, logInfo } from '@edx/frontend-platform/logging'; | ||
| import { useCallback, useMemo } from 'react'; | ||
| import { useGoogleReCaptcha } from 'react-google-recaptcha-v3'; | ||
|
|
||
| export const RECAPTCHA_STATUS = { | ||
| READY: 'ready', | ||
| LOADING: 'loading', | ||
| DISABLED: 'disabled', | ||
| } as const; | ||
| export type RecaptchaStatus = typeof RECAPTCHA_STATUS[keyof typeof RECAPTCHA_STATUS]; | ||
|
|
||
| export const RECAPTCHA_ACTIONS = { | ||
| SIGNUP: 'signup', | ||
| } as const; | ||
| export type KnownRecaptchaAction = typeof RECAPTCHA_ACTIONS[keyof typeof RECAPTCHA_ACTIONS]; | ||
| export type RecaptchaAction = KnownRecaptchaAction | (string & {}); | ||
|
|
||
| const MSG = { | ||
| NOT_READY: (action: RecaptchaAction) => `reCAPTCHA not ready for action: ${action}. Proceeding without token.`, | ||
| TOKEN_FAIL: (action: RecaptchaAction) => `Failed to obtain reCAPTCHA verification token for action: ${action}. | ||
| Please try again or contact support if the issue persists.`, | ||
brobro10000 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| EXEC_FAIL: 'Failed to execute reCAPTCHA', | ||
| } as const; | ||
|
|
||
| /** Return type of the hook */ | ||
| export interface UseRecaptchaTokenResult { | ||
| getToken: () => Promise<string | null>; | ||
| status: RecaptchaStatus; | ||
| /** Convenience booleans */ | ||
| isReady: boolean; | ||
| isLoading: boolean; | ||
| } | ||
|
|
||
| const DEFAULT_ACTION: KnownRecaptchaAction = RECAPTCHA_ACTIONS.SIGNUP; | ||
|
|
||
| const useRecaptchaToken = (actionName: RecaptchaAction = DEFAULT_ACTION): UseRecaptchaTokenResult => { | ||
| const { executeRecaptcha } = useGoogleReCaptcha(); | ||
| const { RECAPTCHA_SITE_KEY_WEB } = getConfig(); | ||
|
|
||
| const status: RecaptchaStatus = useMemo(() => { | ||
| if (!RECAPTCHA_SITE_KEY_WEB) { return RECAPTCHA_STATUS.DISABLED; } | ||
| if (!executeRecaptcha) { return RECAPTCHA_STATUS.LOADING; } | ||
| return RECAPTCHA_STATUS.READY; | ||
| }, [RECAPTCHA_SITE_KEY_WEB, executeRecaptcha]); | ||
brobro10000 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const executeWithFallback = useCallback(async () => { | ||
| if (status === RECAPTCHA_STATUS.READY) { | ||
| const token = await executeRecaptcha!(actionName); | ||
| if (!token) { | ||
| throw new Error(MSG.TOKEN_FAIL(actionName)); | ||
| } | ||
| return token; | ||
| } | ||
|
|
||
| // Fallback: site key exists but recaptcha not initialized yet, or disabled | ||
| if (status !== RECAPTCHA_STATUS.DISABLED) { | ||
| logInfo(MSG.NOT_READY(actionName)); | ||
| } | ||
| return null; | ||
| }, [status, executeRecaptcha, actionName]); | ||
|
|
||
| const getToken = useCallback(async (): Promise<string | null> => { | ||
| try { | ||
| return await executeWithFallback(); | ||
| } catch (err: unknown) { | ||
| const message = (err as { message?: string })?.message ?? MSG.EXEC_FAIL; | ||
| logError(message); | ||
| return null; | ||
| } | ||
| }, [executeWithFallback]); | ||
|
|
||
| return { | ||
| getToken, | ||
| status, | ||
| isReady: status === RECAPTCHA_STATUS.READY, | ||
| isLoading: status === RECAPTCHA_STATUS.LOADING, | ||
| }; | ||
| }; | ||
|
|
||
| export default useRecaptchaToken; | ||
Uh oh!
There was an error while loading. Please reload this page.