Skip to content
Brok UI

Loading…

No results

Auth

Open source

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

12 of 12
Layout

resources/views/blocks/auth-oauth-consent.blade.php Blade
@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

terminal
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

section card avatar alert badge form button brand

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.

auth-oauth-consent.md
# 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
root app and account marks heading unverified warning account row scope list redirect note deny form approve form
Theming hooks
card alert warning badge warning button outline and default

Usage

Render the block as a component, passing data where useful:

resources/views/welcome.blade.php Blade
<x-blocks.auth-oauth-consent />

The controls above the preview are props on this composition. What you picked reads:

Built from primitives

Composes <brok:*> Semantic tokens Re-themeable RTL-ready

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.

resources/views/blocks/auth-oauth-consent.blade.php Blade
@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