Skip to content

[TASK-16189] feat: add browser not supported modal on invites page#1397

Merged
Zishan-7 merged 5 commits intopeanut-wallet-devfrom
fix/invites-page-passkey
Nov 7, 2025
Merged

[TASK-16189] feat: add browser not supported modal on invites page#1397
Zishan-7 merged 5 commits intopeanut-wallet-devfrom
fix/invites-page-passkey

Conversation

@Zishan-7
Copy link
Contributor

@Zishan-7 Zishan-7 commented Nov 4, 2025

No description provided.

@vercel
Copy link

vercel bot commented Nov 4, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
peanut-wallet Ready Ready Preview Comment Nov 7, 2025 5:20am

@notion-workspace
Copy link

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 4, 2025

Walkthrough

Adds a PasskeySupport context/provider and a Jest mock for simplewebauthn; refactors UnsupportedBrowserModal to consume the new context (with loading state and timeout cleanup), and renders the modal (non-closable) inside InvitesPageLayout.

Changes

Cohort / File(s) Change summary
Invites page
src/components/Invites/InvitesPage.tsx
Imports and renders UnsupportedBrowserModal inside InvitesPageLayout with allowClose={false}.
UnsupportedBrowserModal refactor
src/components/Global/UnsupportedBrowserModal/index.tsx
Switched from direct hook to usePasskeySupportContext, added isLoadingPasskeySupport guard, replaced setTimeout with copyTimeoutRef + cleanup, updated effect deps and copy-to-clipboard flow.
Passkey support context & provider
src/context/passkeySupportContext.tsx, src/context/contextProvider.tsx
New PasskeySupportContext, PasskeySupportProvider, and usePasskeySupportContext (client-side); ContextProvider now wraps children with PasskeySupportProvider.
WebAuthn test mock & Jest mapping
src/utils/__mocks__/simplewebauthn-browser.ts, package.json
Added a mock implementation for @simplewebauthn/browser and a moduleNameMapper entry in package.json to point to the mock.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Verify effect dependencies and loading guard in the modal to avoid incorrect visibility/flashing.
  • Confirm PasskeySupportProvider correctly forwards the original hook result and that usePasskeySupportContext throws only when appropriate.
  • Check copy-to-clipboard timeout lifecycle and cleanup (clear on unmount and before resetting).

Possibly related PRs

Suggested reviewers

  • Hugo0
  • kushagrasarathe

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description check ❓ Inconclusive No pull request description was provided by the author, making it impossible to assess whether a description exists or relates to the changeset. Add a pull request description explaining the changes, motivation, and testing performed to help reviewers understand the implementation.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding a browser-not-supported modal (UnsupportedBrowserModal component) to the invites page, which is the primary objective of this PR.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/invites-page-passkey

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai bot added the enhancement New feature or request label Nov 4, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between efaa823 and d70cd93.

📒 Files selected for processing (1)
  • src/components/Invites/InvitesPage.tsx (3 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1072
File: src/app/(setup)/setup/page.tsx:173-175
Timestamp: 2025-08-07T12:53:50.946Z
Learning: In the peanut-ui setup flow at `src/app/(setup)/setup/page.tsx`, when handling unsupported scenarios, both device not supported and browser not supported cases should show the same "Unsupported browser" message using the `UnsupportedBrowserModal` component, rather than having distinct messaging for each scenario.
📚 Learning: 2025-08-07T12:53:50.946Z
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1072
File: src/app/(setup)/setup/page.tsx:173-175
Timestamp: 2025-08-07T12:53:50.946Z
Learning: In the peanut-ui setup flow at `src/app/(setup)/setup/page.tsx`, when handling unsupported scenarios, both device not supported and browser not supported cases should show the same "Unsupported browser" message using the `UnsupportedBrowserModal` component, rather than having distinct messaging for each scenario.

Applied to files:

  • src/components/Invites/InvitesPage.tsx
🧬 Code graph analysis (1)
src/components/Invites/InvitesPage.tsx (1)
src/hooks/usePasskeySupport.ts (1)
  • usePasskeySupport (21-102)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview
🔇 Additional comments (1)
src/components/Invites/InvitesPage.tsx (1)

18-19: LGTM! Imports are correct.

The necessary imports for browser support gating are properly added.

@Hugo0
Copy link
Contributor

Hugo0 commented Nov 7, 2025

@coderabbitai review in depth.

Evaluate:

  • risk of introducing new bugs and breaking existing functionality
  • performance impact and latency
  • code quality and DRY

Also, write a very concise testing guide for QA

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 7, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Get the modified files content
echo "=== InvitesPage.tsx ==="
cat src/components/Invites/InvitesPage.tsx

echo -e "\n=== UnsupportedBrowserModal/index.tsx ==="
cat src/components/Global/UnsupportedBrowserModal/index.tsx

# Check the usePasskeySupport hook implementation
echo -e "\n=== usePasskeySupport hook ==="
fd -e ts -e tsx "usePasskeySupport" --exec cat {}

# Check if there are other usages of UnsupportedBrowserModal
echo -e "\n=== Other UnsupportedBrowserModal usages ==="
rg -n "UnsupportedBrowserModal" -C2

# Check the modal component structure
echo -e "\n=== Modal component (if exists) ==="
fd -e tsx -e ts "Modal" src/components/Global/ --exec cat {}

Length of output: 17355


@Hugo0, I've completed an in-depth review of this PR. Here's my analysis:

🔴 Risk Assessment: MEDIUM-HIGH

Critical Issues:

  1. Duplicate Hook Execution - Performance & Resource Waste

    // InvitesPage.tsx - First call
    const { isSupported: isPasskeySupported, isLoading: isCheckingPasskeySupport } = usePasskeySupport()
    
    // UnsupportedBrowserModal/index.tsx - Second call (inside the modal)
    const { isSupported: isPasskeySupported, isLoading: isLoadingPasskeySupport } = usePasskeySupport()

    Impact: The usePasskeySupport hook performs an async WebAuthn check via PublicKeyCredential.isConditionalMediationAvailable(). Running this twice simultaneously is wasteful and could cause unnecessary latency, especially on slower devices.

  2. Redundant Detection Logic - DRY Violation

    The modal now has two mechanisms for detecting unsupported browsers:

    • Internal detection: useEffect in the modal sets showInAppBrowserModalViaDetection
    • External control: visible prop passed from parent

    The modal displays when EITHER is true:

    if (!showInAppBrowserModalViaDetection && !visible) {
        return null
    }

    This is confusing and unnecessary. The modal should have a single responsibility.

  3. Memory Leak in Copy Handler

    setTimeout(() => setCopyButtonText('Copy Link'), 2000)

    If the component unmounts before 2 seconds, this will attempt to update unmounted component state. Should add cleanup:

    useEffect(() => {
        let timeoutId: NodeJS.Timeout
        // ... in copy handler
        timeoutId = setTimeout(() => setCopyButtonText('Copy Link'), 2000)
        
        return () => {
            if (timeoutId) clearTimeout(timeoutId)
        }
    }, [])

Non-Breaking Changes:

✅ Existing /setup/page.tsx usage unaffected (still works with visible={true} and allowClose={false})
✅ No API signature changes to exported functions
✅ Proper loading state handling prevents premature modal display


⚡ Performance Impact: LOW-MEDIUM

Concerns:

  • 2x async WebAuthn checks on InvitesPage load (duplicate hook calls)
  • No caching mechanism for passkey support results
  • Each page that uses this pattern will re-run full detection

Latency:

  • WebAuthn check typically takes 50-200ms
  • Duplicate calls = ~100-400ms total
  • User won't notice on fast devices, but could be noticeable on slower devices or poor network conditions

Recommendation: Lift the hook to a context provider and cache results globally:

// Create PasskeySupportProvider
const PasskeySupportContext = createContext<PasskeySupportResult | null>(null)

export const PasskeySupportProvider = ({ children }) => {
    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
}

🎨 Code Quality & DRY: NEEDS IMPROVEMENT

DRY Violations:

  1. Duplicate passkey detection (as mentioned above)

  2. Inconsistent modal usage pattern:

    • /setup/page.tsx: Uses modal with visible={true} directly
    • InvitesPage.tsx: Uses hook to compute visibility externally

    This inconsistency makes the modal's API unclear.

Recommendations:

Option A - Self-Contained Modal (Preferred):
Remove the hook from parent and let modal handle everything internally:

// InvitesPage.tsx - Remove usePasskeySupport
<UnsupportedBrowserModal allowClose={false} />

// Modal handles detection internally - no visible prop needed

Option B - Parent-Controlled Modal:
Remove internal detection from modal entirely:

// Remove usePasskeySupport from UnsupportedBrowserModal
// Rely only on visible prop from parent
<UnsupportedBrowserModal allowClose={false} visible={!isCheckingPasskeySupport && !isPasskeySupported} />

I recommend Option A - keeps the modal self-contained and reduces parent complexity.

Positive Aspects:

✅ Proper Suspense boundary to prevent hydration errors
✅ Good error handling in usePasskeySupport hook
✅ Clear variable naming and TypeScript types
✅ Correct usage of allowClose={false} to prevent modal dismissal


🧪 QA Testing Guide

Test Scenarios:

1. Supported Browser (Desktop)

  • Browser: Chrome/Edge/Safari (latest versions)
  • Steps:
    1. Navigate to /invites?code=VALID_CODE
    2. Wait for page load
  • Expected: Page loads normally, NO modal appears

2. Unsupported Browser

  • Browser: Firefox on iOS, Opera Mini, or older browsers without WebAuthn
  • Steps:
    1. Navigate to /invites?code=VALID_CODE
    2. Observe modal appearance
  • Expected:
    • Modal appears immediately after loading completes
    • Title: "Open this link in your browser"
    • "Copy Link" button is clickable
    • Close button (X) is hidden
    • Cannot dismiss modal by clicking outside

3. In-App Browser

  • Apps to test: Instagram, Facebook, Twitter, TikTok, Snapchat
  • Steps:
    1. Share invite link in app chat
    2. Click link (opens in-app browser)
  • Expected:
    • Modal blocks interaction
    • "Copy Link" button copies full URL
    • User must open in external browser

4. Copy Link Functionality

  • Steps:
    1. Trigger unsupported browser modal
    2. Click "Copy Link" button
    3. Check clipboard contents
  • Expected:
    • Button text changes to "Copied!" for 2 seconds
    • Toast notification appears: "Link copied to clipboard!"
    • Clipboard contains correct URL (with redirect_uri if present)

5. With redirect_uri Parameter

  • Steps:
    1. Navigate to /invites?code=VALID_CODE&redirect_uri=%2Fprofile
    2. Trigger unsupported browser modal
    3. Click "Copy Link"
  • Expected: Copied URL is {origin}/profile (decoded redirect_uri)

6. Loading State

  • Network: Slow 3G throttling
  • Steps: Load invites page
  • Expected:
    • Loading spinner shows first
    • Modal only appears AFTER loading completes
    • No flash of modal during loading

7. Already Logged-In User with App Access

  • Precondition: User logged in with hasAppAccess: true
  • Steps: Navigate to /invites?code=VALID_CODE
  • Expected: Redirects to /{username} (inviter's profile), no modal

8. Invalid Invite Code

  • Steps: Navigate to /invites?code=INVALID
  • Expected: Shows error view "Invalid Invite Code", no browser modal

📋 Summary

Aspect Rating Notes
Bug Risk 🟡 Medium Duplicate hooks, minor memory leak
Performance 🟡 Medium 2x async checks, no caching
Code Quality 🟠 Low DRY violations, confusing API
Breaking Changes 🟢 None Existing functionality preserved

Recommendation: Refactor to eliminate duplicate hook calls before merging. The functionality works but has technical debt that should be addressed.


🧠 Learnings used
Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 852
File: src/hooks/useTransactionHistory.ts:177-239
Timestamp: 2025-05-14T21:16:06.369Z
Learning: When reviewing code, pay attention to and resolve CodeRabbit comments as they can contain useful refactoring suggestions and insights.

Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 594
File: src/components/Request/Pay/Views/Initial.view.tsx:188-213
Timestamp: 2024-12-31T15:08:15.640Z
Learning: Always resolve coderabbit comments when reviewing code, since they can contain useful suggestions.

Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 942
File: src/components/AddMoney/consts/index.ts:2151-2162
Timestamp: 2025-06-30T10:44:08.048Z
Learning: Hugo0 often agrees with refactoring suggestions but defers implementation due to time constraints, preferring to track improvements as follow-up issues when they're part of larger architectural changes.

Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 823
File: src/context/kernelClient.context.tsx:79-86
Timestamp: 2025-04-29T19:36:38.121Z
Learning: When Hugo0 asks to "resolve coderabbit comments", they want to acknowledge the comment without necessarily implementing the suggested changes, as the current implementation might be intentional for their specific use case.

Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1072
File: src/app/(setup)/setup/page.tsx:173-175
Timestamp: 2025-08-07T12:53:50.946Z
Learning: In the peanut-ui setup flow at `src/app/(setup)/setup/page.tsx`, when handling unsupported scenarios, both device not supported and browser not supported cases should show the same "Unsupported browser" message using the `UnsupportedBrowserModal` component, rather than having distinct messaging for each scenario.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/utils/__mocks__/simplewebauthn-browser.ts (1)

1-32: Consider adding explicit return types for better type safety.

The mocks use simplified data types (strings instead of ArrayBuffer/Uint8Array) which is acceptable for testing, but adding explicit TypeScript return types would improve type safety and make the mock contract clearer. Since these are jest.fn() wrappers, individual tests can still override these implementations for error scenarios.

Example for improved type safety:

import type { 
  RegistrationResponseJSON, 
  AuthenticationResponseJSON 
} from '@simplewebauthn/browser'

export const startRegistration = jest.fn((): Promise<RegistrationResponseJSON> =>
  Promise.resolve({
    id: 'mock-credential-id',
    rawId: 'mock-raw-id',
    response: {
      clientDataJSON: 'mock-client-data',
      attestationObject: 'mock-attestation',
    },
    type: 'public-key',
  } as RegistrationResponseJSON)
)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9e86ec6 and 01955a6.

📒 Files selected for processing (2)
  • package.json (1 hunks)
  • src/utils/__mocks__/simplewebauthn-browser.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1072
File: src/app/(setup)/setup/page.tsx:173-175
Timestamp: 2025-08-07T12:53:50.946Z
Learning: In the peanut-ui setup flow at `src/app/(setup)/setup/page.tsx`, when handling unsupported scenarios, both device not supported and browser not supported cases should show the same "Unsupported browser" message using the `UnsupportedBrowserModal` component, rather than having distinct messaging for each scenario.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview
🔇 Additional comments (3)
package.json (1)

152-152: LGTM! Jest mock mapping correctly configured.

The moduleNameMapper entry follows the established pattern and correctly points to the new mock implementation.

src/utils/__mocks__/simplewebauthn-browser.ts (2)

34-40: Encoding/decoding mocks are appropriately simplified for testing.

These utility functions return dummy values rather than performing actual encoding/decoding, which is acceptable for most test scenarios. Tests requiring specific encoded values can override these mocks.


42-53: LGTM! Error and service classes properly implemented.

The WebAuthnError class correctly extends Error with proper name assignment. The WebAuthnAbortService uses a real AbortController rather than a mock, which is acceptable since AbortController is a standard browser API available in the test environment.

@Zishan-7 Zishan-7 merged commit 92ae996 into peanut-wallet-dev Nov 7, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants