From d70cd93acf30f3b081e8623b72bccd5519ebb31c Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Tue, 4 Nov 2025 17:15:35 +0530 Subject: [PATCH 1/5] feat: add browser not supported modal on invites page --- src/components/Invites/InvitesPage.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/Invites/InvitesPage.tsx b/src/components/Invites/InvitesPage.tsx index 116fef800..bc8eeade5 100644 --- a/src/components/Invites/InvitesPage.tsx +++ b/src/components/Invites/InvitesPage.tsx @@ -15,6 +15,8 @@ import { useAuth } from '@/context/authContext' import { EInviteType } from '@/services/services.types' import { saveToCookie } from '@/utils' import { useLogin } from '@/hooks/useLogin' +import UnsupportedBrowserModal from '../Global/UnsupportedBrowserModal' +import { usePasskeySupport } from '@/hooks/usePasskeySupport' function InvitePageContent() { const searchParams = useSearchParams() @@ -25,6 +27,7 @@ function InvitePageContent() { const dispatch = useAppDispatch() const router = useRouter() const { handleLoginClick, isLoggingIn } = useLogin() + const { isSupported: isPasskeySupported } = usePasskeySupport() const { data: inviteCodeData, @@ -108,6 +111,7 @@ function InvitePageContent() { + ) } From 511b48244052f96d00812e87654b40dd700171dc Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Tue, 4 Nov 2025 19:51:39 +0530 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=90=9B=20fix:=20update=20passkey=20su?= =?UTF-8?q?pport=20check=20in=20invites=20page=20modal=20visibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Invites/InvitesPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Invites/InvitesPage.tsx b/src/components/Invites/InvitesPage.tsx index bc8eeade5..6d47b9cc7 100644 --- a/src/components/Invites/InvitesPage.tsx +++ b/src/components/Invites/InvitesPage.tsx @@ -27,7 +27,7 @@ function InvitePageContent() { const dispatch = useAppDispatch() const router = useRouter() const { handleLoginClick, isLoggingIn } = useLogin() - const { isSupported: isPasskeySupported } = usePasskeySupport() + const { isSupported: isPasskeySupported, isLoading: isCheckingPasskeySupport } = usePasskeySupport() const { data: inviteCodeData, @@ -111,7 +111,7 @@ function InvitePageContent() { - + ) } From a7557f3ebbcf63ee10938efab1112d10eecd42f3 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Thu, 6 Nov 2025 23:20:47 +0530 Subject: [PATCH 3/5] fix: unsupported browser modal visibility --- src/components/Global/UnsupportedBrowserModal/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Global/UnsupportedBrowserModal/index.tsx b/src/components/Global/UnsupportedBrowserModal/index.tsx index a344920e7..29f178eac 100644 --- a/src/components/Global/UnsupportedBrowserModal/index.tsx +++ b/src/components/Global/UnsupportedBrowserModal/index.tsx @@ -47,13 +47,13 @@ const UnsupportedBrowserModalContent = ({ const [showInAppBrowserModalViaDetection, setShowInAppBrowserModalViaDetection] = useState(false) const [copyButtonText, setCopyButtonText] = useState('Copy Link') const toast = useToast() - const { isSupported: isPasskeySupported } = usePasskeySupport() + const { isSupported: isPasskeySupported, isLoading: isLoadingPasskeySupport } = usePasskeySupport() useEffect(() => { - if (!isPasskeySupported) { + if (!isPasskeySupported && !isLoadingPasskeySupport) { setShowInAppBrowserModalViaDetection(true) } - }, [isPasskeySupported]) + }, [isPasskeySupported, isLoadingPasskeySupport]) if (!showInAppBrowserModalViaDetection && !visible) { return null From 9e86ec6934a63b3c909d756b9a27d796c70b1651 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Fri, 7 Nov 2025 10:32:51 +0530 Subject: [PATCH 4/5] fix: memory leak and create passkey context for improved performance --- .../Global/UnsupportedBrowserModal/index.tsx | 23 +++++++++++++++---- src/components/Invites/InvitesPage.tsx | 4 +--- src/context/contextProvider.tsx | 5 +++- src/context/passkeySupportContext.tsx | 16 +++++++++++++ 4 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 src/context/passkeySupportContext.tsx diff --git a/src/components/Global/UnsupportedBrowserModal/index.tsx b/src/components/Global/UnsupportedBrowserModal/index.tsx index 29f178eac..a387025aa 100644 --- a/src/components/Global/UnsupportedBrowserModal/index.tsx +++ b/src/components/Global/UnsupportedBrowserModal/index.tsx @@ -4,9 +4,9 @@ import ActionModal, { type ActionModalButtonProps } from '@/components/Global/Ac import { useToast } from '@/components/0_Bruddle/Toast' import { type IconName } from '@/components/Global/Icons/Icon' import { copyTextToClipboardWithFallback } from '@/utils/general.utils' -import { useEffect, useState, Suspense } from 'react' +import { useEffect, useState, Suspense, useRef } from 'react' import { useSearchParams } from 'next/navigation' -import { usePasskeySupport } from '@/hooks/usePasskeySupport' +import { usePasskeySupportContext } from '@/context/passkeySupportContext' export const inAppSignatures = [ 'WebView', @@ -47,7 +47,8 @@ const UnsupportedBrowserModalContent = ({ const [showInAppBrowserModalViaDetection, setShowInAppBrowserModalViaDetection] = useState(false) const [copyButtonText, setCopyButtonText] = useState('Copy Link') const toast = useToast() - const { isSupported: isPasskeySupported, isLoading: isLoadingPasskeySupport } = usePasskeySupport() + const { isSupported: isPasskeySupported, isLoading: isLoadingPasskeySupport } = usePasskeySupportContext() + const copyTimeoutRef = useRef(null) useEffect(() => { if (!isPasskeySupported && !isLoadingPasskeySupport) { @@ -55,6 +56,15 @@ const UnsupportedBrowserModalContent = ({ } }, [isPasskeySupported, isLoadingPasskeySupport]) + // Cleanup timeout on unmount to prevent memory leak + useEffect(() => { + return () => { + if (copyTimeoutRef.current) { + clearTimeout(copyTimeoutRef.current) + } + } + }, []) + if (!showInAppBrowserModalViaDetection && !visible) { return null } @@ -72,6 +82,11 @@ const UnsupportedBrowserModalContent = ({ iconPosition: 'left', onClick: async () => { try { + // Clear any existing timeout to prevent multiple resets + if (copyTimeoutRef.current) { + clearTimeout(copyTimeoutRef.current) + } + // copy the redirect uri if it exists, otherwise copy the current url const redirectUri = searchParams.get('redirect_uri') const urlToCopy = redirectUri @@ -80,7 +95,7 @@ const UnsupportedBrowserModalContent = ({ await copyTextToClipboardWithFallback(urlToCopy) setCopyButtonText('Copied!') toast.success('Link copied to clipboard!') - setTimeout(() => setCopyButtonText('Copy Link'), 2000) + copyTimeoutRef.current = setTimeout(() => setCopyButtonText('Copy Link'), 2000) } catch (err) { console.error('Failed to copy: ', err) toast.error('Failed to copy link.') diff --git a/src/components/Invites/InvitesPage.tsx b/src/components/Invites/InvitesPage.tsx index 6d47b9cc7..6bf3df974 100644 --- a/src/components/Invites/InvitesPage.tsx +++ b/src/components/Invites/InvitesPage.tsx @@ -16,7 +16,6 @@ import { EInviteType } from '@/services/services.types' import { saveToCookie } from '@/utils' import { useLogin } from '@/hooks/useLogin' import UnsupportedBrowserModal from '../Global/UnsupportedBrowserModal' -import { usePasskeySupport } from '@/hooks/usePasskeySupport' function InvitePageContent() { const searchParams = useSearchParams() @@ -27,7 +26,6 @@ function InvitePageContent() { const dispatch = useAppDispatch() const router = useRouter() const { handleLoginClick, isLoggingIn } = useLogin() - const { isSupported: isPasskeySupported, isLoading: isCheckingPasskeySupport } = usePasskeySupport() const { data: inviteCodeData, @@ -111,7 +109,7 @@ function InvitePageContent() { - + ) } diff --git a/src/context/contextProvider.tsx b/src/context/contextProvider.tsx index 01dcb29c7..7cc96ea31 100644 --- a/src/context/contextProvider.tsx +++ b/src/context/contextProvider.tsx @@ -9,6 +9,7 @@ import { WithdrawFlowContextProvider } from './WithdrawFlowContext' import { ClaimBankFlowContextProvider } from './ClaimBankFlowContext' import { RequestFulfilmentFlowContextProvider } from './RequestFulfillmentFlowContext' import { SupportModalProvider } from './SupportModalContext' +import { PasskeySupportProvider } from './passkeySupportContext' export const ContextProvider = ({ children }: { children: React.ReactNode }) => { return ( @@ -22,7 +23,9 @@ export const ContextProvider = ({ children }: { children: React.ReactNode }) => - {children} + + {children} + diff --git a/src/context/passkeySupportContext.tsx b/src/context/passkeySupportContext.tsx new file mode 100644 index 000000000..110314b86 --- /dev/null +++ b/src/context/passkeySupportContext.tsx @@ -0,0 +1,16 @@ +'use client' +import { usePasskeySupport, type PasskeySupportResult } from '@/hooks/usePasskeySupport' +import { createContext, useContext } from 'react' + +const PasskeySupportContext = createContext(null) + +export const PasskeySupportProvider = ({ children }: { children: React.ReactNode }) => { + const support = usePasskeySupport() + return {children} +} + +export const usePasskeySupportContext = () => { + const context = useContext(PasskeySupportContext) + if (!context) throw new Error('usePasskeySupportContext must be used within PasskeySupportProvider') + return context +} From 01955a6e78ff6cf69e5198a49f73dbd8b08d7866 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Fri, 7 Nov 2025 10:45:37 +0530 Subject: [PATCH 5/5] add mock for `@simplewebauthn/browser` --- package.json | 1 + src/utils/__mocks__/simplewebauthn-browser.ts | 53 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 src/utils/__mocks__/simplewebauthn-browser.ts diff --git a/package.json b/package.json index ed31e3a45..c724027df 100644 --- a/package.json +++ b/package.json @@ -149,6 +149,7 @@ "^web-push$": "/src/utils/__mocks__/web-push.ts", "^next/cache$": "/src/utils/__mocks__/next-cache.ts", "^@zerodev/sdk(.*)$": "/src/utils/__mocks__/zerodev-sdk.ts", + "^@simplewebauthn/browser$": "/src/utils/__mocks__/simplewebauthn-browser.ts", "^@/(.*)$": "/src/$1" }, "setupFilesAfterEnv": [ diff --git a/src/utils/__mocks__/simplewebauthn-browser.ts b/src/utils/__mocks__/simplewebauthn-browser.ts new file mode 100644 index 000000000..18179aa0e --- /dev/null +++ b/src/utils/__mocks__/simplewebauthn-browser.ts @@ -0,0 +1,53 @@ +// Mock for @simplewebauthn/browser +export const browserSupportsWebAuthn = jest.fn(() => true) + +export const browserSupportsWebAuthnAutofill = jest.fn(() => Promise.resolve(true)) + +export const startRegistration = jest.fn(() => + Promise.resolve({ + id: 'mock-credential-id', + rawId: 'mock-raw-id', + response: { + clientDataJSON: 'mock-client-data', + attestationObject: 'mock-attestation', + }, + type: 'public-key', + }) +) + +export const startAuthentication = jest.fn(() => + Promise.resolve({ + id: 'mock-credential-id', + rawId: 'mock-raw-id', + response: { + clientDataJSON: 'mock-client-data', + authenticatorData: 'mock-auth-data', + signature: 'mock-signature', + userHandle: 'mock-user-handle', + }, + type: 'public-key', + }) +) + +export const platformAuthenticatorIsAvailable = jest.fn(() => Promise.resolve(true)) + +export const base64URLStringToBuffer = jest.fn((base64URLString: string) => { + return new ArrayBuffer(8) +}) + +export const bufferToBase64URLString = jest.fn((buffer: ArrayBuffer) => { + return 'mock-base64-string' +}) + +export class WebAuthnError extends Error { + constructor(message: string) { + super(message) + this.name = 'WebAuthnError' + } +} + +export class WebAuthnAbortService { + static createNewAbortSignal() { + return new AbortController().signal + } +}