Skip to content
Open
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
93 changes: 64 additions & 29 deletions frontend/src/components/pairing/NearbyPairRequestDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from '@/i18n'
import { shortFingerprint } from '@/lib/fingerprint'
import {
formatDeviceTypeLabel,
isKnownPairedEndpoint,
respondNearbyInvite,
respondPairRequest,
} from '@/lib/pairing-api'
import { IS_PAIRING_CAPABLE } from '@/lib/platform'
import { shortFingerprint } from '@/lib/fingerprint'
import { listen } from '@/lib/platform-api'
import {
pairingDataHydrated,
usePairingDataStore,
} from '@/store/pairing-data-store'
import {
AlertDialog,
AlertDialogClose,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
Expand All @@ -26,6 +25,7 @@ import { VerificationCode } from './VerificationCode'

export type NearbyPairRequestPayload = {
remote_endpoint_id: string
session_id: string
sender_name: string
device_type: string
os?: string
Expand All @@ -38,6 +38,10 @@ export type NearbyPairRequestPayload = {
export function NearbyPairRequestDialog() {
const { t } = useTranslation()
const [request, setRequest] = useState<NearbyPairRequestPayload | null>(null)
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
/** Suppresses the controlled dialog close event after a programmatic close. */
const handledRef = useRef(false)

useEffect(() => {
if (!IS_PAIRING_CAPABLE) return
Expand All @@ -64,6 +68,9 @@ export function NearbyPairRequestDialog() {
if (isKnownPairedEndpoint(devices, payload.remote_endpoint_id)) {
return
}
handledRef.current = false
setError(null)
setSubmitting(false)
setRequest(payload)
})()
}
Expand All @@ -85,25 +92,48 @@ export function NearbyPairRequestDialog() {

if (!IS_PAIRING_CAPABLE) return null

const notify = (endpointId: string, accepted: boolean) => {
void respondNearbyInvite(endpointId, accepted).catch(() => {
// Best-effort; UI already proceeded.
})
}

const decline = () => {
setRequest((current) => {
if (!current) return current
notify(current.remote_endpoint_id, false)
return null
})
const decline = async () => {
const current = request
if (!current || submitting) return
handledRef.current = true
setSubmitting(true)
setError(null)
try {
await respondPairRequest(
current.remote_endpoint_id,
current.session_id ?? '',
false
)
setRequest(null)
} catch (err) {
handledRef.current = false
setError(String(err))
} finally {
setSubmitting(false)
}
}

const accept = () => {
const accept = async () => {
const current = request
if (!current) return
setRequest(null)
notify(current.remote_endpoint_id, true)
if (!current || submitting) return
handledRef.current = true
setSubmitting(true)
setError(null)
try {
await respondPairRequest(
current.remote_endpoint_id,
current.session_id ?? '',
true
)
setRequest(null)
} catch (err) {
// Keep the dialog open so the user can retry; a silent failure here
// used to leave the sender paired and this device not.
handledRef.current = false
setError(String(err))
} finally {
setSubmitting(false)
}
}

const typeLabel = request ? formatDeviceTypeLabel(request.device_type) : ''
Expand All @@ -117,7 +147,7 @@ export function NearbyPairRequestDialog() {
<AlertDialog
open={request != null}
onOpenChange={(open) => {
if (!open) decline()
if (!open && !submitting && !handledRef.current) void decline()
}}
>
<AlertDialogContent>
Expand All @@ -138,6 +168,11 @@ export function NearbyPairRequestDialog() {
: ''}
</AlertDialogDescription>
</AlertDialogHeader>
{error ? (
<p className="px-6 pb-2 text-sm text-destructive" role="alert">
{t('common:receiver.pairFailed')}: {error}
</p>
) : null}
<div className="px-6 pb-4">
{/* Guarded on `request`: the popup stays mounted through its close
animation, so an unguarded render flashes the "no code" error
Expand All @@ -155,15 +190,15 @@ export function NearbyPairRequestDialog() {
) : null}
</div>
<AlertDialogFooter>
<AlertDialogClose
render={
<Button size="sm" variant="outline">
{t('common:receiver.declineInvite')}
</Button>
}
<Button
size="sm"
variant="outline"
onClick={decline}
/>
<Button size="sm" onClick={accept}>
disabled={submitting}
>
{t('common:receiver.declineInvite')}
</Button>
<Button size="sm" onClick={accept} disabled={submitting}>
{t('common:receiver.acceptInvite')}
</Button>
</AlertDialogFooter>
Expand Down
103 changes: 103 additions & 0 deletions frontend/src/components/receiver/PairPromptDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { useEffect, useRef, useState } from 'react'

import { useTranslation } from '@/i18n'
import { Button } from '../ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '../ui/dialog'
import { useReceiverContext } from './ReceiverProvider'

/**
* Global post-transfer pairing prompt.
*
* Lives above the routed screens (inside `ReceiverProvider`) so a prompt that
* arrives while the user is on Settings or Devices is not lost. Pairing is
* asynchronous: this dialog only disables itself while the request is being
* delivered; the actual result arrives as `device-paired`.
*/
export function PairPromptDialog() {
const { t } = useTranslation()
const { pairPrompt, acceptPairPrompt, dismissPairPrompt } =

Check failure on line 25 in frontend/src/components/receiver/PairPromptDialog.tsx

View workflow job for this annotation

GitHub Actions / Lint & format

Property 'dismissPairPrompt' does not exist on type 'UseReceiverReturn'.

Check failure on line 25 in frontend/src/components/receiver/PairPromptDialog.tsx

View workflow job for this annotation

GitHub Actions / Lint & format

Property 'acceptPairPrompt' does not exist on type 'UseReceiverReturn'.

Check failure on line 25 in frontend/src/components/receiver/PairPromptDialog.tsx

View workflow job for this annotation

GitHub Actions / Lint & format

Property 'pairPrompt' does not exist on type 'UseReceiverReturn'.
useReceiverContext()
Comment on lines +25 to +26

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Major · Functional Correctness · Heavy lift

Supported by static analysis

Script executed:

rg -n 'UseReceiverReturn|pairPrompt|acceptPairPrompt|dismissPairPrompt' frontend/src/components/receiver frontend/src/hooks | head -100

Repository: tonyantony300/dashbeam

Length of output: 1898


Script executed:

set -e
echo "== useReceiver.ts interface region =="
nl -ba frontend/src/hooks/useReceiver.ts | sed -n '85,135p'
echo "== pairPrompt / acceptPairPrompt / dismissPairPrompt across frontend =="
rg -n -F -e 'pairPrompt' -e 'acceptPairPrompt' -e 'dismissPairPrompt' frontend/src || test "$?" -eq 1
echo "== ReceiverProvider.tsx =="
nl -ba frontend/src/components/receiver/ReceiverProvider.tsx | sed -n '1,40p'
echo "== diff stat vs merge base for useReceiver.ts and ReceiverProvider.tsx =="
git diff --stat b41eddb16b226667de6f8dd7f1326a0ce6bf5e7d a9e826e8212fbb1d698571c77441649ca5fceada -- frontend/src/hooks/useReceiver.ts frontend/src/components/receiver/ReceiverProvider.tsx || test "$?" -eq 1
git show a9e826e8212fbb1d698571c77441649ca5fceada:frontend/src/hooks/useReceiver.ts | grep -n -F -e 'pairPrompt' -e 'PairPrompt' || test "$?" -eq 1

Repository: tonyantony300/dashbeam

Length of output: 4666


Add the pairing prompt members to UseReceiverReturn.

PairPromptDialog reads pairPrompt, acceptPairPrompt, and dismissPairPrompt from useReceiverContext(). That context is typed as UseReceiverReturn, which does not declare these members. The TypeScript check fails with TS2339, so the PR cannot pass typecheck.

No definition of these three members exists in frontend/src. The hook and provider need to expose prompt state and both handlers, with the return type updated to match.

Tools
GitHub Actions: Lint & Format CI / 0_Lint & format.txt

[error] 25-25: TypeScript (tsc, run via pnpm run lint) error TS2339: pairPrompt, acceptPairPrompt, and dismissPairPrompt do not exist on type UseReceiverReturn.

GitHub Actions: Lint & Format CI / Lint & format

[error] 25-25: TypeScript error TS2339: UseReceiverReturn does not contain pairPrompt, acceptPairPrompt, or dismissPairPrompt. pnpm run lint failed during tsc --noEmit --skipLibCheck.

GitHub Check: Lint & format

[failure] 25-25:
Property 'dismissPairPrompt' does not exist on type 'UseReceiverReturn'.


[failure] 25-25:
Property 'acceptPairPrompt' does not exist on type 'UseReceiverReturn'.


[failure] 25-25:
Property 'pairPrompt' does not exist on type 'UseReceiverReturn'.

const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
/** Suppresses the controlled Dialog's close event after a programmatic close. */
const handledRef = useRef(false)

// A new sender (or a retry after dismissal) starts clean.
useEffect(() => {
if (!pairPrompt) return
handledRef.current = false
setError(null)
setSubmitting(false)
}, [pairPrompt])

if (!pairPrompt) return null

const accept = async () => {
// `acceptPairPrompt` clears the prompt on success; mark this as a
// programmatic close so onOpenChange does not record a dismissal.
handledRef.current = true
setSubmitting(true)
setError(null)
try {
// The hook keeps the prompt open when the request cannot be
// delivered, so the user can retry.
const sent = await acceptPairPrompt()
if (!sent) {
handledRef.current = false
setError(t('common:receiver.pairFailed'))
}
} finally {
setSubmitting(false)
}
}

const dismiss = () => {
handledRef.current = true
dismissPairPrompt()
}

return (
<Dialog
open
onOpenChange={(open) => {
if (!open && !submitting && !handledRef.current) dismiss()
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('common:receiver.pairPromptTitle')}</DialogTitle>
<DialogDescription>
{t('common:receiver.pairPromptDescription', {
name: pairPrompt.displayName,
})}
</DialogDescription>
</DialogHeader>
{error ? (
<p className="text-sm text-destructive" role="alert">
{error}
</p>
) : null}
<DialogFooter>
<Button
variant="outline"
size="sm"
onClick={dismiss}
disabled={submitting}
>
{t('common:receiver.pairPromptDecline')}
</Button>
<Button size="sm" onClick={accept} disabled={submitting}>
{t('common:receiver.pairPromptAccept')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
Loading
Loading