Auth
Authentication screens — sign in / sign up with split brand panels, testimonial sidebars and patterned backdrops, plus two-factor, password-recovery and email-verification cards.
Preview
@props([
// 'page' centres the card in its own section; 'card' renders only the card
// for a host guest layout that already frames the page.
'layout' => 'page',
// layout="page" only: true fills the viewport height (min-h-svh) and
// centres the card in it, as auth-sign-in does; false keeps a 36rem floor.
'fullHeight' => false,
// The authorization server's product name for a brand lockup (the brand
// item); null shows none. The `brand` slot replaces the lockup.
'brand' => null,
'brandHref' => null,
// The requesting application: ['name' => …, 'logo' => url|null, 'url' => homepage|null].
'client' => ['name' => 'Example App', 'logo' => null, 'url' => null],
// Where the user is sent after a decision. A full redirect URI is reduced to its host.
'redirect' => 'app.example.com',
// 'host' (default) shows only the redirect host; 'full' shows the whole
// redirect URI, for clients whose callback path or port matters.
'redirectDisplay' => 'host',
// Mark a client the authorization server has not verified.
'unverified' => false,
// The signed-in account and the link to sign in as someone else.
'account' => ['name' => 'Alex Morgan', 'email' => 'alex@example.com', 'avatar' => null],
// What the account row names: 'email', 'name' or 'both' (name over email).
'accountDisplay' => 'email',
// false or '' hides the switch link (Blade turns null into the default).
'switchAccountHref' => '#',
// [['name' => 'projects:read', 'description' => 'See your projects', 'sensitive' => false], …].
// Null renders example scopes for previews; an authorization server always
// passes the requested scopes ([] shows the basic-profile line).
'scopes' => null,
// Approve and deny targets. Laravel Passport: POST and DELETE on /oauth/authorize.
'approveAction' => '/oauth/authorize',
'denyAction' => '/oauth/authorize',
'denyMethod' => 'DELETE',
// Hidden fields both forms post, e.g. ['state' => …, 'client_id' => …, 'auth_token' => …].
'hidden' => [],
'headingTag' => 'h1',
// Visible copy. Every text goes through __(); :app is the client name.
'heading' => ':app wants to access your account',
'unverifiedTitle' => 'This app is not verified',
'unverifiedDescription' => 'We have not confirmed who runs :app. Only continue if you know and trust the developer.',
'accountLabel' => 'Signed in as',
'switchAccountLabel' => 'Switch account',
'scopesIntro' => ':app will be able to:',
'emptyScopesLabel' => 'See your basic profile: your name and email address.',
'sensitiveLabel' => 'Sensitive',
'redirectLabel' => 'After you decide, you will be sent to',
'aboutLabel' => 'About :app',
'newTabLabel' => '(opens in a new tab)',
'denyLabel' => 'Deny',
'approveLabel' => 'Approve',
])
{{--
Auth OAuth Consent — the authorization screen of an OAuth server. It names
the requesting app (with an optional logo and an unverified warning), the
host it sends the user back to, the signed-in account with a switch link and
every requested scope with a plain description; sensitive scopes are
flagged. Deny and Approve are the same size, side by side, and each posts
its own form with the hidden fields.
Slots: `brand` replaces the product lockup (shown above the card with
layout="page", at the top of the card header with layout="card"), `logo`
replaces the client mark, `avatar` replaces the account
avatar, `identity` replaces the text of the account row. The output has no
inline style, script or event handler, so it runs under a strict
Content-Security-Policy.
--}}
@php
$asCard = $layout === 'card';
$headingTag = in_array($headingTag, ['h1', 'h2'], true) ? $headingTag : 'h1';
$accountDisplay = in_array($accountDisplay, ['email', 'name', 'both'], true) ? $accountDisplay : 'email';
$showsSwitch = $switchAccountHref !== false && filled($switchAccountHref);
$client = (array) $client;
$clientName = (string) ($client['name'] ?? __('This app'));
$redirectDisplay = in_array($redirectDisplay, ['host', 'full'], true) ? $redirectDisplay : 'host';
$redirectHost = $redirectDisplay === 'full'
? (string) $redirect
: (string) (parse_url((string) $redirect, PHP_URL_HOST) ?: $redirect);
$account = (array) $account;
$accountName = (string) ($account['name'] ?? '');
$accountEmail = (string) ($account['email'] ?? '');
$accountPrimary = match ($accountDisplay) {
'name', 'both' => $accountName !== '' ? $accountName : $accountEmail,
default => $accountEmail !== '' ? $accountEmail : $accountName,
};
$accountSecondary = $accountDisplay === 'both' && $accountName !== '' && $accountEmail !== '' ? $accountEmail : null;
$scopes ??= [
['name' => 'profile', 'description' => __('See your name, email address and avatar')],
['name' => 'projects:read', 'description' => __('See your projects, issues and comments')],
['name' => 'issues:write', 'description' => __('Create and update issues on your behalf'), 'sensitive' => true],
];
$scopes = array_values(array_filter((array) $scopes, static fn ($scope): bool => is_array($scope) && filled($scope['description'] ?? $scope['name'] ?? null)));
$initials = collect(explode(' ', (string) ($account['name'] ?? $account['email'] ?? '?')))
->filter()->take(2)->map(static fn (string $part): string => mb_strtoupper(mb_substr($part, 0, 1)))->implode('');
$clientInitial = mb_strtoupper(mb_substr($clientName, 0, 1));
$uid = 'oauth-consent-'.\Illuminate\Support\Str::random(6);
$fullHeight = filter_var($fullHeight, FILTER_VALIDATE_BOOLEAN);
// A `brand` slot arrives in $brand as a ComponentSlot; a string is a label.
$brandSlot = $brand instanceof \Illuminate\View\ComponentSlot ? $brand : null;
$hasBrand = $brandSlot !== null ? $brandSlot->hasActualContent() : filled($brand);
$brandLockup = null;
@endphp
@if ($hasBrand)
@php ob_start(); @endphp
<div data-slot="oauth-consent-brand" class="flex justify-center">
@if ($brandSlot !== null)
{{ $brandSlot }}
@else
<x-ui.brand :label="(string) $brand" :href="filled($brandHref) ? $brandHref : null" />
@endif
</div>
@php $brandLockup = new \Illuminate\Support\HtmlString(ob_get_clean()); @endphp
@endif
@php ob_start(); @endphp
<x-ui.card
@class(['w-full', 'max-w-md' => ! $asCard])
:attributes="$asCard ? $attributes : new \Illuminate\View\ComponentAttributeBag()"
>
<x-ui.card.header class="items-center gap-4 text-center">
@if ($asCard && $brandLockup !== null)
{{ $brandLockup }}
@endif
<div class="flex items-center gap-4" aria-hidden="true">
@isset($logo)
{{ $logo }}
@elseif (filled($client['logo'] ?? null))
<img src="{{ $client['logo'] }}" alt="" class="size-12 rounded-md border border-border object-cover" />
@else
<span class="grid size-12 place-items-center rounded-md bg-muted text-xl font-semibold text-foreground">{{ $clientInitial }}</span>
@endif
<svg class="size-5 text-muted-foreground rtl:-scale-x-100" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3 4 7l4 4M4 7h16M16 21l4-4-4-4M20 17H4" /></svg>
@isset($avatar)
{{ $avatar }}
@else
<x-ui.avatar :src="$account['avatar'] ?? null" :fallback="$initials" size="lg" />
@endisset
</div>
<{{ $headingTag }} id="{{ $uid }}-heading" class="text-xl font-semibold tracking-tight text-card-foreground">
{{ __($heading, ['app' => $clientName]) }}
</{{ $headingTag }}>
</x-ui.card.header>
<x-ui.card.content class="flex flex-col gap-6">
@if ($unverified)
<x-ui.alert variant="warning">
<x-ui.alert.title>{{ __($unverifiedTitle, ['app' => $clientName]) }}</x-ui.alert.title>
@if (filled($unverifiedDescription))
<x-ui.alert.description>{{ __($unverifiedDescription, ['app' => $clientName]) }}</x-ui.alert.description>
@endif
</x-ui.alert>
@endif
<div data-slot="oauth-consent-account" class="flex min-w-0 items-center justify-between gap-4 rounded-md border border-border px-4 py-2">
<div class="min-w-0">
@isset($identity)
{{ $identity }}
@else
@if (filled($accountLabel))
<p class="text-xs text-muted-foreground">{{ __($accountLabel) }}</p>
@endif
<p class="truncate text-sm font-medium text-foreground">{{ $accountPrimary }}</p>
@if ($accountSecondary !== null)
<p data-slot="oauth-consent-account-email" class="truncate text-xs text-muted-foreground">{{ $accountSecondary }}</p>
@endif
@endisset
</div>
@if ($showsSwitch)
<a href="{{ $switchAccountHref }}" class="shrink-0 text-sm font-medium text-link hover:underline">{{ __($switchAccountLabel) }}</a>
@endif
</div>
<div>
<p id="{{ $uid }}-scopes" class="text-sm font-medium text-foreground">{{ __($scopesIntro, ['app' => $clientName]) }}</p>
@if ($scopes === [])
<p class="mt-2 text-sm text-muted-foreground">{{ __($emptyScopesLabel, ['app' => $clientName]) }}</p>
@else
<ul role="list" aria-labelledby="{{ $uid }}-scopes" class="mt-2 flex flex-col divide-y divide-border">
@foreach ($scopes as $scope)
<li data-slot="oauth-consent-scope" @if (! empty($scope['sensitive'])) data-sensitive @endif class="flex min-w-0 items-start gap-4 py-2">
<svg class="mt-0.5 size-4 shrink-0 {{ ! empty($scope['sensitive']) ? 'text-warning-text' : 'text-muted-foreground' }}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
@if (! empty($scope['sensitive']))
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3M12 9v4M12 17h.01" />
@else
<path d="m5 12 5 5L20 7" />
@endif
</svg>
<div class="min-w-0 flex-1">
<p class="break-words text-sm text-foreground">{{ $scope['description'] ?? $scope['name'] }}</p>
@if (filled($scope['name'] ?? null) && filled($scope['description'] ?? null))
<p class="break-all font-mono text-xs text-muted-foreground">{{ $scope['name'] }}</p>
@endif
</div>
@if (! empty($scope['sensitive']))
<x-ui.badge variant="warning" size="sm" class="shrink-0">{{ __($sensitiveLabel) }}</x-ui.badge>
@endif
</li>
@endforeach
</ul>
@endif
</div>
@if (filled($redirectHost) || filled($client['url'] ?? null))
<p data-slot="oauth-consent-redirect" class="text-xs text-muted-foreground">
@if (filled($redirectHost))
{{ __($redirectLabel, ['app' => $clientName]) }}
@if ($redirectDisplay === 'full')
<span dir="ltr" data-redirect-display="full" class="break-all font-mono font-medium text-foreground">{{ $redirectHost }}</span>.
@else
<span dir="ltr" class="break-words font-medium text-foreground">{{ $redirectHost }}</span>.
@endif
@endif
@if (filled($client['url'] ?? null))
<a href="{{ $client['url'] }}" class="text-link hover:underline" rel="noopener noreferrer" target="_blank">{{ __($aboutLabel, ['app' => $clientName]) }}<span class="sr-only"> {{ __($newTabLabel) }}</span></a>
@endif
</p>
@endif
{{-- Deny first in reading order, same size as Approve: declining is as easy as agreeing. --}}
<div class="grid grid-cols-2 gap-4">
<x-ui.form :action="$denyAction" :method="$denyMethod" data-decision="deny">
@foreach ((array) $hidden as $field => $value)
<input type="hidden" name="{{ $field }}" value="{{ $value }}" />
@endforeach
<x-ui.button type="submit" variant="outline" class="w-full">{{ __($denyLabel, ['app' => $clientName]) }}</x-ui.button>
</x-ui.form>
<x-ui.form :action="$approveAction" method="POST" data-decision="approve">
@foreach ((array) $hidden as $field => $value)
<input type="hidden" name="{{ $field }}" value="{{ $value }}" />
@endforeach
<x-ui.button type="submit" class="w-full">{{ __($approveLabel, ['app' => $clientName]) }}</x-ui.button>
</x-ui.form>
</div>
</x-ui.card.content>
</x-ui.card>
@php $card = new \Illuminate\Support\HtmlString(ob_get_clean()); @endphp
@if ($asCard)
{{ $card }}
@else
<x-ui.section
data-slot="block-auth-oauth-consent"
aria-labelledby="{{ $uid }}-heading"
space="md"
@class([
'flex items-center justify-center px-4',
'min-h-svh' => $fullHeight,
'min-h-[36rem]' => ! $fullHeight,
'flex-col gap-6' => $brandLockup !== null,
])
{{ $attributes }}
>
{{ $brandLockup }}
{{ $card }}
</x-ui.section>
@endif
Installation
php artisan ui:add blocks/auth-oauth-consent
Registry contract
Install confidence
php artisan ui:add blocks/auth-oauth-consent
writes only the generated targets below. The CLI validates each file hash before writing and
prompts before replacing local changes unless --force is used.
- Version
- 1.3.0
- License
- open
- Stability
- stable
- Contract
- v4
- Foundation
- ≥ 1.0.0
| Type | Generated target |
|---|---|
| blade | resources/views/blocks/auth-oauth-consent.blade.php |
Registry dependencies
Package dependencies
composer: jml/brok:^0.2
Use with AI
A brief for your coding agent: what the block is, the install command, how to render it, its props and the rules. Copy it, or open a prompt about this block in an assistant.
# Brok UI block: Auth Oauth Consent (`auth-oauth-consent`)
The authorization screen of an OAuth server: the requesting app with an optional logo and unverified warning, the redirect host, the signed-in account with a switch link, every requested scope with a plain description and sensitive flag, and equal Deny and Approve forms.
Brok UI is a Laravel Blade registry. `ui:add` copies this block into the app as plain Blade the app owns; it composes installed `<brok:*>` primitives and semantic design tokens.
## Install
```bash
php artisan ui:add blocks/auth-oauth-consent
```
## Render it
```blade
<x-blocks.auth-oauth-consent layout="page" />
```
## Props
- `layout` (page|card, default `page`) (variant) — 'page' centres the card in its own section; 'card' renders only the card for a host guest layout that already frames the page. Host attributes go to the section in page layout and to the card in card layout.
- `full-height` (boolean, default `false`) — layout="page" only: fills the viewport height (min-h-svh) and centres the card in it, like auth-sign-in; false keeps the 36rem floor.
- `brand` (string|null, default `null`) — Product name shown as a brand lockup (the brand item): above the card with layout="page", at the top of the card header with layout="card". The brand slot replaces the lockup (for example a logo).
- `brand-href` (string|null, default `null`) — Makes the brand lockup a link.
- `client` (array, default `['name' => 'Example App', 'logo' => null, 'url' => null]`) — The requesting app: name, optional logo URL and optional homepage (adds an About link that opens in a new tab).
- `redirect` (string, default `app.example.com`) — Where the decision sends the person. A full redirect URI is reduced to its host; '' hides the sentence.
- `redirect-display` ('host'|'full', default `host`) — How the redirect target shows: 'host' reduces a full URI to its host; 'full' shows the whole redirect URI (left to right, breaking anywhere) when the path or port matters.
- `unverified` (bool, default `false`) — Show a warning that the authorization server has not verified the client.
- `account` (array, default `['name' => 'Alex Morgan', 'email' => '[email protected]', 'avatar' => null]`) — The signed-in account: name, email and optional avatar URL (shown with the avatar component).
- `account-display` (email|name|both, default `email`) — What the account row names: the email, the name, or the name with the email under it.
- `switch-account-href` (string|false, default `#`) — Link to sign in as someone else before deciding; false or '' hides it.
- `scopes` (array|null, default `null`) — Requested scopes: name, human description and optional sensitive flag. Null shows example scopes; [] shows the basic-profile line.
- `approve-action` (string, default `/oauth/authorize`) — POST target of Approve (Laravel Passport: POST /oauth/authorize).
- `deny-action` (string, default `/oauth/authorize`) — Target of Deny (Laravel Passport: DELETE /oauth/authorize).
- `deny-method` (string, default `DELETE`) — Deny verb; PUT, PATCH and DELETE are method-spoofed.
- `hidden` (array, default `[]`) — Hidden fields both forms post, for example state, client_id and auth_token.
- `heading-tag` (string, default `h1`) — Heading level: h1 or h2.
- `heading` (string, default `:app wants to access your account`) — Heading, passed through __() with :app as the client name.
- `unverified-title` (string, default `This app is not verified`) — Title of the unverified warning.
- `unverified-description` (string, default `We have not confirmed who runs :app. Only continue if you know and trust the developer.`) — Text of the unverified warning; empty hides it.
- `account-label` (string, default `Signed in as`) — Line above the account; empty hides it.
- `switch-account-label` (string, default `Switch account`) — Switch-account link text, for example 'Not you? Switch account'.
- `scopes-intro` (string, default `:app will be able to:`) — Lead-in sentence that labels the scope list.
- `empty-scopes-label` (string, default `See your basic profile: your name and email address.`) — Text shown when scopes is [].
- `sensitive-label` (string, default `Sensitive`) — Badge text of a sensitive scope.
- `redirect-label` (string, default `After you decide, you will be sent to`) — Text before the redirect host.
- `about-label` (string, default `About :app`) — About-link text when client.url is set.
- `new-tab-label` (string, default `(opens in a new tab)`) — Screen-reader note of the About link.
- `deny-label` (string, default `Deny`) — Deny button label.
- `approve-label` (string, default `Approve`) — Approve button label, for example 'Allow read access'.
## Use when
- Your app is an OAuth authorization server (Laravel Passport or your own) and must ask the signed-in person to approve a third-party client.
## Avoid when
- Signing in with another provider (the client side of OAuth); use social-button in auth-sign-in.
- Managing apps that already have access; use settings-integrations.
## Rules
- Render the installed block with `<x-blocks.auth-oauth-consent />` and pass data through its props; edit the copied file only for structural changes.
- Keep the semantic design tokens (`bg-background`, `text-muted-foreground`); never swap in raw colour utilities.
- Keep the `data-slot` attributes and the logical (start/end) spacing so the markup still mirrors under `dir="rtl"`.
## Links
- Docs: https://brokui.dev/blocks/auth-oauth-consent
- Registry JSON (files, props, contract): https://brokui.dev/r/open/blocks/auth-oauth-consent.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
Guidance
Use when
- Your app is an OAuth authorization server (Laravel Passport or your own) and must ask the signed-in person to approve a third-party client.
Avoid when
- Signing in with another provider (the client side of OAuth); use social-button in auth-sign-in.
- Managing apps that already have access; use settings-integrations.
Anti-patterns
- Do not replace semantic props with conflicting utility classes.
- Do not remove labels, focus styles, or state attributes.
- Anatomy
- Theming hooks
Usage
Render the block as a component, passing data where useful:
<x-blocks.auth-oauth-consent />
The controls above the preview are props on this composition. What you picked reads:
Built from primitives
This block composes installed <brok:*> primitives and semantic design
tokens only — it does not reimplement any primitive. Re-theme it (light, dark, admin, customer) by
editing CSS variables; flip the preview to RTL to confirm it mirrors.
Source
The exact, editable file ui:add writes
into your app. The preview above renders this same source — there are no preview-only blocks.
@props([
// 'page' centres the card in its own section; 'card' renders only the card
// for a host guest layout that already frames the page.
'layout' => 'page',
// layout="page" only: true fills the viewport height (min-h-svh) and
// centres the card in it, as auth-sign-in does; false keeps a 36rem floor.
'fullHeight' => false,
// The authorization server's product name for a brand lockup (the brand
// item); null shows none. The `brand` slot replaces the lockup.
'brand' => null,
'brandHref' => null,
// The requesting application: ['name' => …, 'logo' => url|null, 'url' => homepage|null].
'client' => ['name' => 'Example App', 'logo' => null, 'url' => null],
// Where the user is sent after a decision. A full redirect URI is reduced to its host.
'redirect' => 'app.example.com',
// 'host' (default) shows only the redirect host; 'full' shows the whole
// redirect URI, for clients whose callback path or port matters.
'redirectDisplay' => 'host',
// Mark a client the authorization server has not verified.
'unverified' => false,
// The signed-in account and the link to sign in as someone else.
'account' => ['name' => 'Alex Morgan', 'email' => 'alex@example.com', 'avatar' => null],
// What the account row names: 'email', 'name' or 'both' (name over email).
'accountDisplay' => 'email',
// false or '' hides the switch link (Blade turns null into the default).
'switchAccountHref' => '#',
// [['name' => 'projects:read', 'description' => 'See your projects', 'sensitive' => false], …].
// Null renders example scopes for previews; an authorization server always
// passes the requested scopes ([] shows the basic-profile line).
'scopes' => null,
// Approve and deny targets. Laravel Passport: POST and DELETE on /oauth/authorize.
'approveAction' => '/oauth/authorize',
'denyAction' => '/oauth/authorize',
'denyMethod' => 'DELETE',
// Hidden fields both forms post, e.g. ['state' => …, 'client_id' => …, 'auth_token' => …].
'hidden' => [],
'headingTag' => 'h1',
// Visible copy. Every text goes through __(); :app is the client name.
'heading' => ':app wants to access your account',
'unverifiedTitle' => 'This app is not verified',
'unverifiedDescription' => 'We have not confirmed who runs :app. Only continue if you know and trust the developer.',
'accountLabel' => 'Signed in as',
'switchAccountLabel' => 'Switch account',
'scopesIntro' => ':app will be able to:',
'emptyScopesLabel' => 'See your basic profile: your name and email address.',
'sensitiveLabel' => 'Sensitive',
'redirectLabel' => 'After you decide, you will be sent to',
'aboutLabel' => 'About :app',
'newTabLabel' => '(opens in a new tab)',
'denyLabel' => 'Deny',
'approveLabel' => 'Approve',
])
{{--
Auth OAuth Consent — the authorization screen of an OAuth server. It names
the requesting app (with an optional logo and an unverified warning), the
host it sends the user back to, the signed-in account with a switch link and
every requested scope with a plain description; sensitive scopes are
flagged. Deny and Approve are the same size, side by side, and each posts
its own form with the hidden fields.
Slots: `brand` replaces the product lockup (shown above the card with
layout="page", at the top of the card header with layout="card"), `logo`
replaces the client mark, `avatar` replaces the account
avatar, `identity` replaces the text of the account row. The output has no
inline style, script or event handler, so it runs under a strict
Content-Security-Policy.
--}}
@php
$asCard = $layout === 'card';
$headingTag = in_array($headingTag, ['h1', 'h2'], true) ? $headingTag : 'h1';
$accountDisplay = in_array($accountDisplay, ['email', 'name', 'both'], true) ? $accountDisplay : 'email';
$showsSwitch = $switchAccountHref !== false && filled($switchAccountHref);
$client = (array) $client;
$clientName = (string) ($client['name'] ?? __('This app'));
$redirectDisplay = in_array($redirectDisplay, ['host', 'full'], true) ? $redirectDisplay : 'host';
$redirectHost = $redirectDisplay === 'full'
? (string) $redirect
: (string) (parse_url((string) $redirect, PHP_URL_HOST) ?: $redirect);
$account = (array) $account;
$accountName = (string) ($account['name'] ?? '');
$accountEmail = (string) ($account['email'] ?? '');
$accountPrimary = match ($accountDisplay) {
'name', 'both' => $accountName !== '' ? $accountName : $accountEmail,
default => $accountEmail !== '' ? $accountEmail : $accountName,
};
$accountSecondary = $accountDisplay === 'both' && $accountName !== '' && $accountEmail !== '' ? $accountEmail : null;
$scopes ??= [
['name' => 'profile', 'description' => __('See your name, email address and avatar')],
['name' => 'projects:read', 'description' => __('See your projects, issues and comments')],
['name' => 'issues:write', 'description' => __('Create and update issues on your behalf'), 'sensitive' => true],
];
$scopes = array_values(array_filter((array) $scopes, static fn ($scope): bool => is_array($scope) && filled($scope['description'] ?? $scope['name'] ?? null)));
$initials = collect(explode(' ', (string) ($account['name'] ?? $account['email'] ?? '?')))
->filter()->take(2)->map(static fn (string $part): string => mb_strtoupper(mb_substr($part, 0, 1)))->implode('');
$clientInitial = mb_strtoupper(mb_substr($clientName, 0, 1));
$uid = 'oauth-consent-'.\Illuminate\Support\Str::random(6);
$fullHeight = filter_var($fullHeight, FILTER_VALIDATE_BOOLEAN);
// A `brand` slot arrives in $brand as a ComponentSlot; a string is a label.
$brandSlot = $brand instanceof \Illuminate\View\ComponentSlot ? $brand : null;
$hasBrand = $brandSlot !== null ? $brandSlot->hasActualContent() : filled($brand);
$brandLockup = null;
@endphp
@if ($hasBrand)
@php ob_start(); @endphp
<div data-slot="oauth-consent-brand" class="flex justify-center">
@if ($brandSlot !== null)
{{ $brandSlot }}
@else
<x-ui.brand :label="(string) $brand" :href="filled($brandHref) ? $brandHref : null" />
@endif
</div>
@php $brandLockup = new \Illuminate\Support\HtmlString(ob_get_clean()); @endphp
@endif
@php ob_start(); @endphp
<x-ui.card
@class(['w-full', 'max-w-md' => ! $asCard])
:attributes="$asCard ? $attributes : new \Illuminate\View\ComponentAttributeBag()"
>
<x-ui.card.header class="items-center gap-4 text-center">
@if ($asCard && $brandLockup !== null)
{{ $brandLockup }}
@endif
<div class="flex items-center gap-4" aria-hidden="true">
@isset($logo)
{{ $logo }}
@elseif (filled($client['logo'] ?? null))
<img src="{{ $client['logo'] }}" alt="" class="size-12 rounded-md border border-border object-cover" />
@else
<span class="grid size-12 place-items-center rounded-md bg-muted text-xl font-semibold text-foreground">{{ $clientInitial }}</span>
@endif
<svg class="size-5 text-muted-foreground rtl:-scale-x-100" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3 4 7l4 4M4 7h16M16 21l4-4-4-4M20 17H4" /></svg>
@isset($avatar)
{{ $avatar }}
@else
<x-ui.avatar :src="$account['avatar'] ?? null" :fallback="$initials" size="lg" />
@endisset
</div>
<{{ $headingTag }} id="{{ $uid }}-heading" class="text-xl font-semibold tracking-tight text-card-foreground">
{{ __($heading, ['app' => $clientName]) }}
</{{ $headingTag }}>
</x-ui.card.header>
<x-ui.card.content class="flex flex-col gap-6">
@if ($unverified)
<x-ui.alert variant="warning">
<x-ui.alert.title>{{ __($unverifiedTitle, ['app' => $clientName]) }}</x-ui.alert.title>
@if (filled($unverifiedDescription))
<x-ui.alert.description>{{ __($unverifiedDescription, ['app' => $clientName]) }}</x-ui.alert.description>
@endif
</x-ui.alert>
@endif
<div data-slot="oauth-consent-account" class="flex min-w-0 items-center justify-between gap-4 rounded-md border border-border px-4 py-2">
<div class="min-w-0">
@isset($identity)
{{ $identity }}
@else
@if (filled($accountLabel))
<p class="text-xs text-muted-foreground">{{ __($accountLabel) }}</p>
@endif
<p class="truncate text-sm font-medium text-foreground">{{ $accountPrimary }}</p>
@if ($accountSecondary !== null)
<p data-slot="oauth-consent-account-email" class="truncate text-xs text-muted-foreground">{{ $accountSecondary }}</p>
@endif
@endisset
</div>
@if ($showsSwitch)
<a href="{{ $switchAccountHref }}" class="shrink-0 text-sm font-medium text-link hover:underline">{{ __($switchAccountLabel) }}</a>
@endif
</div>
<div>
<p id="{{ $uid }}-scopes" class="text-sm font-medium text-foreground">{{ __($scopesIntro, ['app' => $clientName]) }}</p>
@if ($scopes === [])
<p class="mt-2 text-sm text-muted-foreground">{{ __($emptyScopesLabel, ['app' => $clientName]) }}</p>
@else
<ul role="list" aria-labelledby="{{ $uid }}-scopes" class="mt-2 flex flex-col divide-y divide-border">
@foreach ($scopes as $scope)
<li data-slot="oauth-consent-scope" @if (! empty($scope['sensitive'])) data-sensitive @endif class="flex min-w-0 items-start gap-4 py-2">
<svg class="mt-0.5 size-4 shrink-0 {{ ! empty($scope['sensitive']) ? 'text-warning-text' : 'text-muted-foreground' }}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
@if (! empty($scope['sensitive']))
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3M12 9v4M12 17h.01" />
@else
<path d="m5 12 5 5L20 7" />
@endif
</svg>
<div class="min-w-0 flex-1">
<p class="break-words text-sm text-foreground">{{ $scope['description'] ?? $scope['name'] }}</p>
@if (filled($scope['name'] ?? null) && filled($scope['description'] ?? null))
<p class="break-all font-mono text-xs text-muted-foreground">{{ $scope['name'] }}</p>
@endif
</div>
@if (! empty($scope['sensitive']))
<x-ui.badge variant="warning" size="sm" class="shrink-0">{{ __($sensitiveLabel) }}</x-ui.badge>
@endif
</li>
@endforeach
</ul>
@endif
</div>
@if (filled($redirectHost) || filled($client['url'] ?? null))
<p data-slot="oauth-consent-redirect" class="text-xs text-muted-foreground">
@if (filled($redirectHost))
{{ __($redirectLabel, ['app' => $clientName]) }}
@if ($redirectDisplay === 'full')
<span dir="ltr" data-redirect-display="full" class="break-all font-mono font-medium text-foreground">{{ $redirectHost }}</span>.
@else
<span dir="ltr" class="break-words font-medium text-foreground">{{ $redirectHost }}</span>.
@endif
@endif
@if (filled($client['url'] ?? null))
<a href="{{ $client['url'] }}" class="text-link hover:underline" rel="noopener noreferrer" target="_blank">{{ __($aboutLabel, ['app' => $clientName]) }}<span class="sr-only"> {{ __($newTabLabel) }}</span></a>
@endif
</p>
@endif
{{-- Deny first in reading order, same size as Approve: declining is as easy as agreeing. --}}
<div class="grid grid-cols-2 gap-4">
<x-ui.form :action="$denyAction" :method="$denyMethod" data-decision="deny">
@foreach ((array) $hidden as $field => $value)
<input type="hidden" name="{{ $field }}" value="{{ $value }}" />
@endforeach
<x-ui.button type="submit" variant="outline" class="w-full">{{ __($denyLabel, ['app' => $clientName]) }}</x-ui.button>
</x-ui.form>
<x-ui.form :action="$approveAction" method="POST" data-decision="approve">
@foreach ((array) $hidden as $field => $value)
<input type="hidden" name="{{ $field }}" value="{{ $value }}" />
@endforeach
<x-ui.button type="submit" class="w-full">{{ __($approveLabel, ['app' => $clientName]) }}</x-ui.button>
</x-ui.form>
</div>
</x-ui.card.content>
</x-ui.card>
@php $card = new \Illuminate\Support\HtmlString(ob_get_clean()); @endphp
@if ($asCard)
{{ $card }}
@else
<x-ui.section
data-slot="block-auth-oauth-consent"
aria-labelledby="{{ $uid }}-heading"
space="md"
@class([
'flex items-center justify-center px-4',
'min-h-svh' => $fullHeight,
'min-h-[36rem]' => ! $fullHeight,
'flex-col gap-6' => $brandLockup !== null,
])
{{ $attributes }}
>
{{ $brandLockup }}
{{ $card }}
</x-ui.section>
@endif
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.3.0 - Status
- Stable
- License
-
open - Deprecation
- Not deprecated
- Contract
-
v4 - Foundation
-
≥ 1.0.0