From e65ff069049026739f74d78c797ed64415cf8a4a Mon Sep 17 00:00:00 2001 From: sibilla merlo Date: Tue, 17 Mar 2026 14:55:33 +0100 Subject: [PATCH] frontend: add vendor widget hooks Extract vendor widget resize/height handling and disclaimer terms state into reusable hooks and use them across market offerings and the Bitsurance widget to remove duplicated iframe shell logic. --- .../src/hooks/vendor-iframe-resize-height.ts | 56 +++++++++++++++++++ .../web/src/hooks/vendor-iframe-terms.ts | 18 ++++++ frontends/web/src/hooks/vendor-iframe.ts | 4 ++ .../web/src/routes/bitsurance/widget.tsx | 46 ++++----------- frontends/web/src/routes/market/bitrefill.tsx | 42 ++------------ frontends/web/src/routes/market/btcdirect.tsx | 43 +++----------- frontends/web/src/routes/market/moonpay.tsx | 39 ++----------- frontends/web/src/routes/market/pocket.tsx | 45 ++++----------- 8 files changed, 115 insertions(+), 178 deletions(-) create mode 100644 frontends/web/src/hooks/vendor-iframe-resize-height.ts create mode 100644 frontends/web/src/hooks/vendor-iframe-terms.ts create mode 100644 frontends/web/src/hooks/vendor-iframe.ts diff --git a/frontends/web/src/hooks/vendor-iframe-resize-height.ts b/frontends/web/src/hooks/vendor-iframe-resize-height.ts new file mode 100644 index 0000000000..3f600573f3 --- /dev/null +++ b/frontends/web/src/hooks/vendor-iframe-resize-height.ts @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 + +import { RefObject, useCallback, useEffect, useRef, useState } from 'react'; + +export type TVendorIframeResizeHeight = { + containerRef: RefObject; + height: number; + iframeLoaded: boolean; + iframeRef: RefObject; + onIframeLoad: () => void; +}; + +export const useVendorIframeResizeHeight = (): TVendorIframeResizeHeight => { + const containerRef = useRef(null); + const iframeRef = useRef(null); + const resizeTimerRef = useRef | null>(null); + + const [height, setHeight] = useState(0); + const [iframeLoaded, setIframeLoaded] = useState(false); + + const onResize = useCallback(() => { + if (resizeTimerRef.current) { + clearTimeout(resizeTimerRef.current); + } + resizeTimerRef.current = setTimeout(() => { + if (!containerRef.current) { + return; + } + setHeight(containerRef.current.offsetHeight); + }, 200); + }, []); + + useEffect(() => { + onResize(); + window.addEventListener('resize', onResize); + return () => { + window.removeEventListener('resize', onResize); + if (resizeTimerRef.current) { + clearTimeout(resizeTimerRef.current); + } + }; + }, [onResize]); + + const onIframeLoad = useCallback(() => { + setIframeLoaded(true); + onResize(); + }, [onResize]); + + return { + containerRef, + height, + iframeLoaded, + iframeRef, + onIframeLoad, + }; +}; diff --git a/frontends/web/src/hooks/vendor-iframe-terms.ts b/frontends/web/src/hooks/vendor-iframe-terms.ts new file mode 100644 index 0000000000..cec942c319 --- /dev/null +++ b/frontends/web/src/hooks/vendor-iframe-terms.ts @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 + +import { Dispatch, SetStateAction, useEffect, useState } from 'react'; + +type TVendorIframeTerms = { + agreedTerms: boolean; + setAgreedTerms: Dispatch>; +}; + +export const useVendorTerms = (agreedByConfig = false): TVendorIframeTerms => { + const [agreedTerms, setAgreedTerms] = useState(agreedByConfig); + + useEffect(() => { + setAgreedTerms(agreedByConfig); + }, [agreedByConfig]); + + return { agreedTerms, setAgreedTerms }; +}; diff --git a/frontends/web/src/hooks/vendor-iframe.ts b/frontends/web/src/hooks/vendor-iframe.ts new file mode 100644 index 0000000000..fb31d8c6ee --- /dev/null +++ b/frontends/web/src/hooks/vendor-iframe.ts @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 + +export { useVendorIframeResizeHeight } from './vendor-iframe-resize-height'; +export { useVendorTerms } from './vendor-iframe-terms'; diff --git a/frontends/web/src/routes/bitsurance/widget.tsx b/frontends/web/src/routes/bitsurance/widget.tsx index d92a106edb..9cf6d9d221 100644 --- a/frontends/web/src/routes/bitsurance/widget.tsx +++ b/frontends/web/src/routes/bitsurance/widget.tsx @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -import { useState, useEffect, createRef, useRef } from 'react'; +import { useEffect, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { RequestAddressV0Message, MessageVersion, parseMessage, serializeMessage, V0MessageType } from 'request-address'; @@ -16,6 +16,7 @@ import { alertUser } from '@/components/alert/Alert'; import { BitsuranceGuide } from './guide'; import { getBitsuranceURL } from '@/api/bitsurance'; import { convertScriptType } from '@/utils/request-addess'; +import { useVendorIframeResizeHeight, useVendorTerms } from '@/hooks/vendor-iframe'; import style from './widget.module.css'; type TProps = { @@ -26,48 +27,21 @@ export const BitsuranceWidget = ({ code }: TProps) => { const navigate = useNavigate(); const { t } = useTranslation(); - const [height, setHeight] = useState(0); - const [iframeLoaded, setIframeLoaded] = useState(false); - const [agreedTerms, setAgreedTerms] = useState(false); - const iframeURL = useLoad(getBitsuranceURL); const config = useLoad(getConfig); const accountInfo = useLoad(getInfo(code)); - const ref = createRef(); - const iframeRef = useRef(null); - let signing = false; - let resizeTimerID: any = undefined; + const { containerRef, height, iframeLoaded, iframeRef, onIframeLoad } = useVendorIframeResizeHeight(); + const { agreedTerms, setAgreedTerms } = useVendorTerms(!!config?.frontend?.skipBitsuranceDisclaimer); + const signingRef = useRef(false); useEffect(() => { - if (config) { - setAgreedTerms(config.frontend.skipBitsuranceDisclaimer); - } - }, [config]); - - useEffect(() => { - onResize(); - window.addEventListener('resize', onResize); window.addEventListener('message', onMessage); - return () => { - window.removeEventListener('resize', onResize); window.removeEventListener('message', onMessage); }; }); - const onResize = () => { - if (resizeTimerID) { - clearTimeout(resizeTimerID); - } - resizeTimerID = setTimeout(() => { - if (!ref.current) { - return; - } - setHeight(ref.current.offsetHeight); - }, 200); - }; - const sendAddressWithXPub = (address: string, sig: string, xpub: string) => { const { current } = iframeRef; @@ -94,7 +68,7 @@ export const BitsuranceWidget = ({ code }: TProps) => { }; const handleRequestAddress = (message: RequestAddressV0Message) => { - signing = true; + signingRef.current = true; const addressType = message.withScriptType ? convertScriptType(message.withScriptType) : ''; const withMessageSignature = message.withMessageSignature ? message.withMessageSignature : ''; const withExtendedPublicKey = !!message.withExtendedPublicKey; @@ -103,7 +77,7 @@ export const BitsuranceWidget = ({ code }: TProps) => { withMessageSignature, code) .then(response => { - signing = false; + signingRef.current = false; if (response.success) { if (withExtendedPublicKey) { const xpub = getXPub(addressType as ScriptType); @@ -148,7 +122,7 @@ export const BitsuranceWidget = ({ code }: TProps) => { case V0MessageType.RequestAddress: // we ignore further signing requests // while there is an ongoing one - if (!signing) { + if (!signingRef.current) { handleRequestAddress(message); } break; @@ -166,7 +140,7 @@ export const BitsuranceWidget = ({ code }: TProps) => {
{t('bitsuranceAccount.title')}} />
-
+
{ !agreedTerms ? ( setAgreedTerms(true)} @@ -177,7 +151,7 @@ export const BitsuranceWidget = ({ code }: TProps) => { {!iframeLoaded && }