From 2381d0d189857c04166b0e08b18f2e1833ade0e5 Mon Sep 17 00:00:00 2001 From: Hugo Montenegro Date: Fri, 17 Jul 2026 13:54:53 +0100 Subject: [PATCH] fix(kyc): actually apply the callback-ref fix (#2440 shipped the test only) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #2440 was meant to fix the card outage but landed the regression test WITHOUT the code change: while proving the test failed against prod, a `git checkout origin/main -- SumsubKycWrapper.tsx` staged the reverted file, and the follow-up commit swept that staged revert in. Net effect: main got a test asserting behaviour that main does not have. Its unit job is red and the outage is still live. Re-applies the fix on top of main, unchanged from what #2440 intended: hold the SDK container in state via a callback ref so the init effect re-runs when the headlessui portal attaches the node a commit after `visible` flips. Without it the effect reads a null ref on its only run, never re-runs (a ref is not reactive and is not a dep), and the SDK is never launched — every user gets an infinite spinner (card_sumsub_opened normal, card_sumsub_completed 0). Verified the honest way this time: the test ALREADY on main fails against main's wrapper (launch called 0 times) and passes with this commit. --- src/components/Kyc/SumsubKycWrapper.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/components/Kyc/SumsubKycWrapper.tsx b/src/components/Kyc/SumsubKycWrapper.tsx index e6b0b5d73e..f9149debcc 100644 --- a/src/components/Kyc/SumsubKycWrapper.tsx +++ b/src/components/Kyc/SumsubKycWrapper.tsx @@ -36,7 +36,11 @@ export const SumsubKycWrapper = ({ const [sdkLoadError, setSdkLoadError] = useState(false) const [isHelpModalOpen, setIsHelpModalOpen] = useState(false) const [modalVariant, setModalVariant] = useState<'stop-verification' | 'trouble'>('trouble') - const sdkContainerRef = useRef(null) + // Callback ref, NOT useRef: the modal renders through a headlessui Portal, so + // the container mounts a commit AFTER `visible` flips true. A plain ref is not + // reactive — the init effect below would read null on its only run and never + // launch the SDK. State re-runs the effect the moment the node attaches. + const [sdkContainer, setSdkContainer] = useState(null) const sdkInstanceRef = useRef(null) const { setIsSupportModalOpen } = useModalsContext() @@ -103,7 +107,7 @@ export const SumsubKycWrapper = ({ // initialize sdk as soon as the modal is visible and all deps are ready useEffect(() => { - if (!visible || !accessToken || !sdkLoaded || !sdkContainerRef.current) return + if (!visible || !accessToken || !sdkLoaded || !sdkContainer) return // clean up previous instance if (sdkInstanceRef.current) { @@ -188,7 +192,7 @@ export const SumsubKycWrapper = ({ }) .build() - sdk.launch(sdkContainerRef.current) + sdk.launch(sdkContainer) sdkInstanceRef.current = sdk // ensure the sdk-created iframe gets camera/microphone permissions. @@ -203,10 +207,10 @@ export const SumsubKycWrapper = ({ } } }) - iframeObserver.observe(sdkContainerRef.current, { childList: true }) + iframeObserver.observe(sdkContainer, { childList: true }) // also patch any iframe that was added before the observer - const existingIframe = sdkContainerRef.current.querySelector('iframe') + const existingIframe = sdkContainer.querySelector('iframe') if (existingIframe && !existingIframe.allow?.includes('camera')) { existingIframe.allow = 'camera; microphone; fullscreen' } @@ -228,7 +232,7 @@ export const SumsubKycWrapper = ({ sdkInstanceRef.current = null } } - }, [visible, accessToken, sdkLoaded, stableOnComplete, stableOnError, stableOnRefreshToken]) + }, [visible, accessToken, sdkLoaded, sdkContainer, stableOnComplete, stableOnError, stableOnRefreshToken]) // reset state when modal closes (the init effect's cleanup already // destroys the SDK instance — visible is one of its deps) @@ -345,7 +349,7 @@ export const SumsubKycWrapper = ({