TypeScriptify useRecaptcha

This commit is contained in:
Robin Townsend 2022-05-27 10:37:27 -04:00
commit af74228f8e
3 changed files with 34 additions and 33 deletions

View file

@ -8,7 +8,7 @@
"build-storybook": "build-storybook", "build-storybook": "build-storybook",
"prettier:check": "prettier -c src", "prettier:check": "prettier -c src",
"prettier:format": "prettier -w src", "prettier:format": "prettier -w src",
"lint:js": "eslint --max-warnings 2 src", "lint:js": "eslint --max-warnings 0 src",
"lint:types": "tsc" "lint:types": "tsc"
}, },
"dependencies": { "dependencies": {
@ -31,6 +31,7 @@
"@react-stately/tree": "^3.2.0", "@react-stately/tree": "^3.2.0",
"@sentry/react": "^6.13.3", "@sentry/react": "^6.13.3",
"@sentry/tracing": "^6.13.3", "@sentry/tracing": "^6.13.3",
"@types/grecaptcha": "^3.0.4",
"@use-gesture/react": "^10.2.11", "@use-gesture/react": "^10.2.11",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"color-hash": "^2.0.1", "color-hash": "^2.0.1",

View file

@ -14,52 +14,49 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { randomString } from "matrix-js-sdk/src/randomstring";
import { useEffect, useCallback, useRef, useState } from "react"; import { useEffect, useCallback, useRef, useState } from "react";
import { randomString } from "matrix-js-sdk/src/randomstring";
declare global {
interface Window {
mxOnRecaptchaLoaded: () => void;
}
}
const RECAPTCHA_SCRIPT_URL = const RECAPTCHA_SCRIPT_URL =
"https://www.recaptcha.net/recaptcha/api.js?onload=mxOnRecaptchaLoaded&render=explicit"; "https://www.recaptcha.net/recaptcha/api.js?onload=mxOnRecaptchaLoaded&render=explicit";
export function useRecaptcha(sitekey) { interface RecaptchaPromiseRef {
resolve: (response: string) => void;
reject: (error: Error) => void;
}
export const useRecaptcha = (sitekey: string) => {
const [recaptchaId] = useState(() => randomString(16)); const [recaptchaId] = useState(() => randomString(16));
const promiseRef = useRef(); const promiseRef = useRef<RecaptchaPromiseRef>();
useEffect(() => { useEffect(() => {
if (!sitekey) { if (!sitekey) return;
return;
}
const onRecaptchaLoaded = () => { const onRecaptchaLoaded = () => {
if (!document.getElementById(recaptchaId)) { if (!document.getElementById(recaptchaId)) return;
return;
}
window.grecaptcha.render(recaptchaId, { window.grecaptcha.render(recaptchaId, {
sitekey, sitekey,
size: "invisible", size: "invisible",
callback: (response) => { callback: (response: string) => promiseRef.current?.resolve(response),
if (promiseRef.current) { // eslint-disable-next-line @typescript-eslint/naming-convention
promiseRef.current.resolve(response); "error-callback": () => promiseRef.current?.reject(new Error()),
}
},
"error-callback": (error) => {
if (promiseRef.current) {
promiseRef.current.reject(error);
}
},
}); });
}; };
if ( if (typeof window.grecaptcha?.render === "function") {
typeof window.grecaptcha !== "undefined" &&
typeof window.grecaptcha.render === "function"
) {
onRecaptchaLoaded(); onRecaptchaLoaded();
} else { } else {
window.mxOnRecaptchaLoaded = onRecaptchaLoaded; window.mxOnRecaptchaLoaded = onRecaptchaLoaded;
if (!document.querySelector(`script[src="${RECAPTCHA_SCRIPT_URL}"]`)) { if (!document.querySelector(`script[src="${RECAPTCHA_SCRIPT_URL}"]`)) {
const scriptTag = document.createElement("script"); const scriptTag = document.createElement("script") as HTMLScriptElement;
scriptTag.src = RECAPTCHA_SCRIPT_URL; scriptTag.src = RECAPTCHA_SCRIPT_URL;
scriptTag.async = true; scriptTag.async = true;
document.body.appendChild(scriptTag); document.body.appendChild(scriptTag);
@ -80,7 +77,7 @@ export function useRecaptcha(sitekey) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const observer = new MutationObserver((mutationsList) => { const observer = new MutationObserver((mutationsList) => {
for (const item of mutationsList) { for (const item of mutationsList) {
if (item.target?.style?.visibility !== "visible") { if ((item.target as HTMLElement)?.style?.visibility !== "visible") {
reject(new Error("Recaptcha dismissed")); reject(new Error("Recaptcha dismissed"));
observer.disconnect(); observer.disconnect();
return; return;
@ -101,7 +98,7 @@ export function useRecaptcha(sitekey) {
window.grecaptcha.execute(); window.grecaptcha.execute();
const iframe = document.querySelector( const iframe = document.querySelector<HTMLIFrameElement>(
'iframe[src*="recaptcha/api2/bframe"]' 'iframe[src*="recaptcha/api2/bframe"]'
); );
@ -111,13 +108,11 @@ export function useRecaptcha(sitekey) {
}); });
} }
}); });
}, [recaptchaId, sitekey]); }, [sitekey]);
const reset = useCallback(() => { const reset = useCallback(() => {
if (window.grecaptcha) { window.grecaptcha?.reset();
window.grecaptcha.reset(); }, []);
}
}, [recaptchaId]);
return { execute, reset, recaptchaId }; return { execute, reset, recaptchaId };
} };

View file

@ -2876,6 +2876,11 @@
"@types/minimatch" "*" "@types/minimatch" "*"
"@types/node" "*" "@types/node" "*"
"@types/grecaptcha@^3.0.4":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/grecaptcha/-/grecaptcha-3.0.4.tgz#3de601f3b0cd0298faf052dd5bd62aff64c2be2e"
integrity sha512-7l1Y8DTGXkx/r4pwU1nMVAR+yD/QC+MCHKXAyEX/7JZhwcN1IED09aZ9vCjjkcGdhSQiu/eJqcXInpl6eEEEwg==
"@types/hast@^2.0.0": "@types/hast@^2.0.0":
version "2.3.4" version "2.3.4"
resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.4.tgz#8aa5ef92c117d20d974a82bdfb6a648b08c0bafc" resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.4.tgz#8aa5ef92c117d20d974a82bdfb6a648b08c0bafc"