Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@
"^web-push$": "<rootDir>/src/utils/__mocks__/web-push.ts",
"^next/cache$": "<rootDir>/src/utils/__mocks__/next-cache.ts",
"^@zerodev/sdk(.*)$": "<rootDir>/src/utils/__mocks__/zerodev-sdk.ts",
"^@simplewebauthn/browser$": "<rootDir>/src/utils/__mocks__/simplewebauthn-browser.ts",
"^@/(.*)$": "<rootDir>/src/$1"
},
"setupFilesAfterEnv": [
Expand Down
27 changes: 21 additions & 6 deletions src/components/Global/UnsupportedBrowserModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -47,13 +47,23 @@ 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 } = usePasskeySupportContext()
const copyTimeoutRef = useRef<NodeJS.Timeout | null>(null)

useEffect(() => {
if (!isPasskeySupported) {
if (!isPasskeySupported && !isLoadingPasskeySupport) {
setShowInAppBrowserModalViaDetection(true)
}
}, [isPasskeySupported])
}, [isPasskeySupported, isLoadingPasskeySupport])

// Cleanup timeout on unmount to prevent memory leak
useEffect(() => {
return () => {
if (copyTimeoutRef.current) {
clearTimeout(copyTimeoutRef.current)
}
}
}, [])

if (!showInAppBrowserModalViaDetection && !visible) {
return null
Expand All @@ -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
Expand All @@ -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.')
Expand Down
2 changes: 2 additions & 0 deletions src/components/Invites/InvitesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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'

function InvitePageContent() {
const searchParams = useSearchParams()
Expand Down Expand Up @@ -108,6 +109,7 @@ function InvitePageContent() {
</div>
</div>
</div>
<UnsupportedBrowserModal allowClose={false} />
</InvitesPageLayout>
)
}
Expand Down
5 changes: 4 additions & 1 deletion src/context/contextProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -22,7 +23,9 @@ export const ContextProvider = ({ children }: { children: React.ReactNode }) =>
<RequestFulfilmentFlowContextProvider>
<WithdrawFlowContextProvider>
<OnrampFlowContextProvider>
<SupportModalProvider>{children}</SupportModalProvider>
<SupportModalProvider>
<PasskeySupportProvider>{children}</PasskeySupportProvider>
</SupportModalProvider>
</OnrampFlowContextProvider>
</WithdrawFlowContextProvider>
</RequestFulfilmentFlowContextProvider>
Expand Down
16 changes: 16 additions & 0 deletions src/context/passkeySupportContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use client'
import { usePasskeySupport, type PasskeySupportResult } from '@/hooks/usePasskeySupport'
import { createContext, useContext } from 'react'

const PasskeySupportContext = createContext<PasskeySupportResult | null>(null)

export const PasskeySupportProvider = ({ children }: { children: React.ReactNode }) => {
const support = usePasskeySupport()
return <PasskeySupportContext.Provider value={support}>{children}</PasskeySupportContext.Provider>
}

export const usePasskeySupportContext = () => {
const context = useContext(PasskeySupportContext)
if (!context) throw new Error('usePasskeySupportContext must be used within PasskeySupportProvider')
return context
}
53 changes: 53 additions & 0 deletions src/utils/__mocks__/simplewebauthn-browser.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading