react-call v2

Permission consent

OAuth-style "do you allow X?" prompt. Resolves with allow or deny — a tagged response, not a boolean.

→ awaiting consent…

The Callable

declared once, mounted in the React tree
Permission.tsx
import { createCallable } from 'react-call'
interface Props {
appName: string
scopes: readonly string[]
}
export const Permission = createCallable<Props, 'allow' | 'deny'>(
({ call, appName, scopes }) => (
<div
role="dialog"
aria-modal="true"
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
>
<div className="w-full max-w-sm rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] p-6 shadow-2xl">
<p className="text-base font-medium text-[var(--color-fg)]">
Allow <span className="text-[var(--color-accent)]">{appName}</span>{' '}
to:
</p>
<ul className="mt-4 space-y-2">
{scopes.map((s) => (
<li
key={s}
className="flex items-start gap-2 text-sm text-[var(--color-fg-muted)]"
>
<span aria-hidden="true" className="text-[var(--color-accent)]">
</span>
{s}
</li>
))}
</ul>
<div className="mt-6 flex items-center justify-end gap-3">
<button
type="button"
onClick={() => call.end('deny')}
className="rounded-md border border-[var(--color-border)] px-4 py-2 text-sm font-medium text-[var(--color-fg-muted)] transition-colors hover:text-[var(--color-fg)]"
>
Deny
</button>
<button
type="button"
onClick={() => call.end('allow')}
className="rounded-md bg-[var(--color-accent)] px-4 py-2 text-sm font-medium text-[var(--color-accent-fg)] transition-colors hover:bg-[var(--color-accent-hover)]"
>
Allow
</button>
</div>
</div>
</div>
),
)
Permission.displayName = 'Permission'

The Root

mounted in your app tree, once
App.tsx
import { Permission } from './Permission'
import { ConnectButton } from './ConnectButton'
export default function App() {
return (
<>
<Permission />
<ConnectButton />
</>
)
}

The caller

anywhere in your app, imperative
ConnectButton.tsx
import { useState } from 'react'
import { Permission } from './Permission'
export const ConnectButton = () => {
const [status, setStatus] = useState<'idle' | 'connected' | 'denied'>('idle')
const handleClick = async () => {
const result = await Permission.call({
appName: 'react-call demo',
scopes: [
'Read your profile',
'Read your repositories',
'Subscribe to webhook events',
],
})
setStatus(result === 'allow' ? 'connected' : 'denied')
}
return (
<div className="flex flex-col items-center gap-3">
<button
type="button"
onClick={handleClick}
disabled={status === 'connected'}
className="rounded-md bg-[var(--color-accent)] px-4 py-2 text-sm font-medium text-[var(--color-accent-fg)] transition-colors hover:bg-[var(--color-accent-hover)] disabled:opacity-50"
>
{status === 'connected' ? 'Connected' : 'Connect with GitHub'}
</button>
<span className="font-mono text-xs text-[var(--color-fg-subtle)]">
{status === 'idle' && '→ awaiting consent…'}
{status === 'connected' && (
<span className="text-[var(--color-accent)]">→ allowed</span>
)}
{status === 'denied' && '→ denied'}
</span>
</div>
)
}

Not every yes/no is a boolean

confirm-dialog returns a boolean, and for “proceed or not” that is right. Consent is different: this Callable resolves with 'allow' | 'deny'. The Response type is part of your API — a named union documents itself at the call site (result === 'allow' beats remembering which way true points) and leaves room to grow ('allow-once', 'manage') without rewriting every caller. Reach past boolean when the two outcomes aren’t a pure true/false, or you expect a third one someday.

The Response set also decides how the user can leave. There is no backdrop-click or Esc handler here on purpose: 'allow' | 'deny' has no “dismissed” variant, so there is deliberately no way to close the prompt without answering. Compare item-picker, whose T | null Response does model dismissal — so it lets you click away. Your Response type and your escape hatches have to agree.

When to reach for it

OAuth-style consent, permission and cookie prompts, “approve these terms” flows — anywhere the outcome is a labelled decision you might store, log, or branch on, not a throwaway yes/no.

Related examples