redesign 2fa setup modal

This commit is contained in:
Dan Harrin
2025-04-28 11:07:05 +01:00
parent 14f03e63e0
commit 17a3dcdbcf
7 changed files with 201 additions and 179 deletions
@@ -48,8 +48,6 @@ class PrimesDemo extends Component implements HasSchemas
->color('neutral'),
UnorderedList::make(fn (): array => array_map(
fn (string $recoveryCode): \Filament\Schemas\Components\Component => Text::make($recoveryCode)
->copyable()
->copyMessage('Copied')
->fontFamily(FontFamily::Mono)
->size(TextSize::ExtraSmall)
->color('neutral'),
+1 -1
View File
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@ return [
'actions' => [
'You can also',
'Click to',
'copy' => [
'label' => 'copy',
@@ -15,9 +15,10 @@ use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Flex;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Image;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Text;
use Filament\Schemas\Components\UnorderedList;
use Filament\Schemas\Components\Wizard;
use Filament\Schemas\Components\Wizard\Step;
use Filament\Support\Enums\FontFamily;
use Filament\Support\Enums\FontWeight;
use Filament\Support\Enums\TextSize;
@@ -55,94 +56,92 @@ class SetUpGoogleTwoFactorAuthenticationAction
->modalIconColor('primary')
->modalHeading(__('filament-panels::auth/multi-factor/google-two-factor/actions/set-up.modal.heading'))
->modalDescription(new HtmlString(Blade::render(__('filament-panels::auth/multi-factor/google-two-factor/actions/set-up.modal.description'))))
->schema(function (Action $action) use ($googleTwoFactorAuthentication): array {
return [
Group::make([
Text::make(__('filament-panels::auth/multi-factor/google-two-factor/actions/set-up.modal.content.qr_code.instruction'))
->modifyWizardUsing(fn (Wizard $wizard) => $wizard->hiddenHeader())
->steps(fn (Action $action): array => [
Step::make('app')
->schema([
Group::make([
Text::make(__('filament-panels::auth/multi-factor/google-two-factor/actions/set-up.modal.content.qr_code.instruction'))
->color('neutral'),
Image::make(
url: fn (): string => $googleTwoFactorAuthentication->generateQrCodeDataUri(decrypt($action->getArguments()['encrypted'])['secret']),
alt: __('filament-panels::auth/multi-factor/google-two-factor/actions/set-up.modal.content.qr_code.alt'),
)
->imageHeight('12rem')
->alignCenter(),
Flex::make([
Text::make(__('filament-panels::auth/multi-factor/google-two-factor/actions/set-up.modal.content.text_code.instruction'))
->color('neutral')
->grow(false),
Text::make(fn (): string => decrypt($action->getArguments()['encrypted'])['secret'])
->fontFamily(FontFamily::Mono)
->color('neutral')
->copyable()
->copyMessage(__('filament-panels::auth/multi-factor/google-two-factor/actions/set-up.modal.content.text_code.messages.copied'))
->grow(false),
])->from('sm'),
])
->dense(),
OneTimeCodeInput::make('code')
->label(__('filament-panels::auth/multi-factor/google-two-factor/actions/set-up.modal.form.code.label'))
->belowContent(__('filament-panels::auth/multi-factor/google-two-factor/actions/set-up.modal.form.code.below_content'))
->validationAttribute(__('filament-panels::auth/multi-factor/google-two-factor/actions/set-up.modal.form.code.validation_attribute'))
->required()
->rule(function () use ($action, $googleTwoFactorAuthentication): Closure {
return function (string $attribute, $value, Closure $fail) use ($action, $googleTwoFactorAuthentication): void {
if ($googleTwoFactorAuthentication->verifyCode($value, decrypt($action->getArguments()['encrypted'])['secret'])) {
return;
}
$fail(__('filament-panels::auth/multi-factor/google-two-factor/actions/set-up.modal.form.code.messages.invalid'));
};
}),
]),
Step::make('recovery')
->schema([
Text::make(__('filament-panels::auth/multi-factor/google-two-factor/actions/set-up.modal.content.recovery_codes.instruction'))
->weight(FontWeight::Bold)
->color('neutral'),
Image::make(
url: fn (): string => $googleTwoFactorAuthentication->generateQrCodeDataUri(decrypt($action->getArguments()['encrypted'])['secret']),
alt: __('filament-panels::auth/multi-factor/google-two-factor/actions/set-up.modal.content.qr_code.alt'),
)
->imageHeight('12rem')
->alignCenter(),
Flex::make([
Text::make(__('filament-panels::auth/multi-factor/google-two-factor/actions/set-up.modal.content.text_code.instruction'))
->color('neutral')
->grow(false),
Text::make(fn (): string => decrypt($action->getArguments()['encrypted'])['secret'])
UnorderedList::make(fn (): array => array_map(
fn (string $recoveryCode): Component => Text::make($recoveryCode)
->fontFamily(FontFamily::Mono)
->color('neutral')
->copyable()
->copyMessage(__('filament-panels::auth/multi-factor/google-two-factor/actions/set-up.modal.content.text_code.messages.copied'))
->grow(false),
])->from('sm'),
Section::make()
->schema([
Text::make(__('filament-panels::auth/multi-factor/google-two-factor/actions/set-up.modal.content.recovery_codes.instruction'))
->weight(FontWeight::Bold)
->color('neutral'),
UnorderedList::make(fn (): array => array_map(
fn (string $recoveryCode): Component => Text::make($recoveryCode)
->copyable()
->copyMessage(__('filament-panels::auth/multi-factor/recovery-codes-modal-content.messages.copied'))
->fontFamily(FontFamily::Mono)
->size(TextSize::ExtraSmall)
->color('neutral'),
decrypt($action->getArguments()['encrypted'] ?? encrypt([]))['recoveryCodes'] ?? [],
))
->size(TextSize::ExtraSmall),
Text::make(function () use ($action): Htmlable {
$recoveryCodes = decrypt($action->getArguments()['encrypted'])['recoveryCodes'];
->size(TextSize::ExtraSmall)
->color('neutral'),
decrypt($action->getArguments()['encrypted'] ?? encrypt([]))['recoveryCodes'] ?? [],
))
->size(TextSize::ExtraSmall),
Text::make(function () use ($action): Htmlable {
$recoveryCodes = decrypt($action->getArguments()['encrypted'])['recoveryCodes'];
return new HtmlString(
__('filament-panels::auth/multi-factor/recovery-codes-modal-content.actions.0') .
' ' .
Action::make('copy')
->label(__('filament-panels::auth/multi-factor/recovery-codes-modal-content.actions.copy.label'))
->link()
->alpineClickHandler('
window.navigator.clipboard.writeText(' . Js::from(implode(PHP_EOL, $recoveryCodes)) . ')
$tooltip(' . Js::from(__('filament-panels::auth/multi-factor/recovery-codes-modal-content.messages.copied')) . ', {
theme: $store.theme,
})
')
->toHtml() .
' ' .
__('filament-panels::auth/multi-factor/recovery-codes-modal-content.actions.1') .
' ' .
Action::make('download')
->label(__('filament-panels::auth/multi-factor/recovery-codes-modal-content.actions.download.label'))
->link()
->url('data:application/octet-stream,' . urlencode(implode(PHP_EOL, $recoveryCodes)))
->extraAttributes(['download' => true])
->toHtml() .
' ' .
__('filament-panels::auth/multi-factor/recovery-codes-modal-content.actions.2')
);
}),
])
->compact()
->secondary()
->visible($googleTwoFactorAuthentication->isRecoverable()),
])
->dense(),
OneTimeCodeInput::make('code')
->label(__('filament-panels::auth/multi-factor/google-two-factor/actions/set-up.modal.form.code.label'))
->belowContent(__('filament-panels::auth/multi-factor/google-two-factor/actions/set-up.modal.form.code.below_content'))
->validationAttribute(__('filament-panels::auth/multi-factor/google-two-factor/actions/set-up.modal.form.code.validation_attribute'))
->required()
->rule(function () use ($action, $googleTwoFactorAuthentication): Closure {
return function (string $attribute, $value, Closure $fail) use ($action, $googleTwoFactorAuthentication): void {
if ($googleTwoFactorAuthentication->verifyCode($value, decrypt($action->getArguments()['encrypted'])['secret'])) {
return;
}
$fail(__('filament-panels::auth/multi-factor/google-two-factor/actions/set-up.modal.form.code.messages.invalid'));
};
return new HtmlString(
__('filament-panels::auth/multi-factor/recovery-codes-modal-content.actions.0') .
' ' .
Action::make('copy')
->label(__('filament-panels::auth/multi-factor/recovery-codes-modal-content.actions.copy.label'))
->link()
->alpineClickHandler('
window.navigator.clipboard.writeText(' . Js::from(implode(PHP_EOL, $recoveryCodes)) . ')
$tooltip(' . Js::from(__('filament-panels::auth/multi-factor/recovery-codes-modal-content.messages.copied')) . ', {
theme: $store.theme,
})
')
->toHtml() .
' ' .
__('filament-panels::auth/multi-factor/recovery-codes-modal-content.actions.1') .
' ' .
Action::make('download')
->label(__('filament-panels::auth/multi-factor/recovery-codes-modal-content.actions.download.label'))
->link()
->url('data:application/octet-stream,' . urlencode(implode(PHP_EOL, $recoveryCodes)))
->extraAttributes(['download' => true])
->toHtml() .
' ' .
__('filament-panels::auth/multi-factor/recovery-codes-modal-content.actions.2')
);
}),
];
})
])
->visible($googleTwoFactorAuthentication->isRecoverable()),
])
->modalSubmitAction(fn (Action $action) => $action
->label(__('filament-panels::auth/multi-factor/google-two-factor/actions/set-up.modal.actions.submit.label')))
->action(function (array $arguments) use ($googleTwoFactorAuthentication): void {
@@ -122,15 +122,19 @@
& .fi-sc-wizard-step {
@apply outline-hidden;
&.fi-active {
@apply mt-6;
}
&:not(.fi-active) {
@apply invisible absolute h-0 overflow-hidden p-0;
}
}
&:not(.fi-sc-wizard-header-hidden) {
& .fi-sc-wizard-step {
&.fi-active {
@apply mt-6;
}
}
}
& .fi-sc-wizard-footer {
@apply flex items-center justify-between gap-x-3;
@@ -5,6 +5,8 @@
$key = $getKey();
$previousAction = $getAction('previous');
$nextAction = $getAction('next');
$steps = $getChildSchema()->getComponents();
$isHeaderHidden = $isHeaderHidden();
@endphp
<div
@@ -33,13 +35,14 @@
->class([
'fi-sc-wizard',
'fi-contained' => $isContained,
'fi-sc-wizard-header-hidden' => $isHeaderHidden,
])
}}
>
<input
type="hidden"
value="{{
collect($getChildSchema()->getComponents())
collect($steps)
->filter(static fn (\Filament\Schemas\Components\Wizard\Step $step): bool => $step->isVisible())
->map(static fn (\Filament\Schemas\Components\Wizard\Step $step): ?string => $step->getKey())
->values()
@@ -48,105 +51,109 @@
x-ref="stepsData"
/>
<ol
@if (filled($label = $getLabel()))
aria-label="{{ $label }}"
@endif
role="list"
x-cloak
x-ref="header"
class="fi-sc-wizard-header"
>
@foreach ($getChildSchema()->getComponents() as $step)
<li
class="fi-sc-wizard-header-step"
x-bind:class="{
'fi-active': getStepIndex(step) === {{ $loop->index }},
'fi-completed': getStepIndex(step) > {{ $loop->index }},
}"
>
<button
type="button"
x-bind:aria-current="getStepIndex(step) === {{ $loop->index }} ? 'step' : null"
x-on:click="step = @js($step->getKey())"
x-bind:disabled="! isStepAccessible(@js($step->getKey()))"
role="step"
class="fi-sc-wizard-header-step-btn"
@if (! $isHeaderHidden)
<ol
@if (filled($label = $getLabel()))
aria-label="{{ $label }}"
@endif
role="list"
x-cloak
x-ref="header"
class="fi-sc-wizard-header"
>
@foreach ($steps as $step)
<li
class="fi-sc-wizard-header-step"
x-bind:class="{
'fi-active': getStepIndex(step) === {{ $loop->index }},
'fi-completed': getStepIndex(step) > {{ $loop->index }},
}"
>
<div class="fi-sc-wizard-header-step-icon-ctn">
@php
$completedIcon = $step->getCompletedIcon();
@endphp
<button
type="button"
x-bind:aria-current="getStepIndex(step) === {{ $loop->index }} ? 'step' : null"
x-on:click="step = @js($step->getKey())"
x-bind:disabled="! isStepAccessible(@js($step->getKey()))"
role="step"
class="fi-sc-wizard-header-step-btn"
>
<div class="fi-sc-wizard-header-step-icon-ctn">
@php
$completedIcon = $step->getCompletedIcon();
@endphp
{{
\Filament\Support\generate_icon_html(
$completedIcon ?? \Filament\Support\Icons\Heroicon::OutlinedCheck,
alias: filled($completedIcon) ? null : 'schema::components.wizard.completed-step',
attributes: new \Illuminate\View\ComponentAttributeBag([
'x-cloak' => 'x-cloak',
'x-show' => "getStepIndex(step) > {$loop->index}",
]),
size: \Filament\Support\Enums\IconSize::Large,
)
}}
@if (filled($icon = $step->getIcon()))
{{
\Filament\Support\generate_icon_html(
$icon,
$completedIcon ?? \Filament\Support\Icons\Heroicon::OutlinedCheck,
alias: filled($completedIcon) ? null : 'schema::components.wizard.completed-step',
attributes: new \Illuminate\View\ComponentAttributeBag([
'x-cloak' => 'x-cloak',
'x-show' => "getStepIndex(step) <= {$loop->index}",
'x-show' => "getStepIndex(step) > {$loop->index}",
]),
size: \Filament\Support\Enums\IconSize::Large,
)
}}
@else
<span
x-show="getStepIndex(step) <= {{ $loop->index }}"
class="fi-sc-wizard-header-step-number"
>
{{ str_pad($loop->index + 1, 2, '0', STR_PAD_LEFT) }}
</span>
@endif
</div>
<div class="fi-sc-wizard-header-step-text">
@if (! $step->isLabelHidden())
<span class="fi-sc-wizard-header-step-label">
{{ $step->getLabel() }}
</span>
@endif
@if (filled($icon = $step->getIcon()))
{{
\Filament\Support\generate_icon_html(
$icon,
attributes: new \Illuminate\View\ComponentAttributeBag([
'x-cloak' => 'x-cloak',
'x-show' => "getStepIndex(step) <= {$loop->index}",
]),
size: \Filament\Support\Enums\IconSize::Large,
)
}}
@else
<span
x-show="getStepIndex(step) <= {{ $loop->index }}"
class="fi-sc-wizard-header-step-number"
>
{{ str_pad($loop->index + 1, 2, '0', STR_PAD_LEFT) }}
</span>
@endif
</div>
@if (filled($description = $step->getDescription()))
<span class="fi-sc-wizard-header-step-description">
{{ $description }}
</span>
@endif
</div>
</button>
<div class="fi-sc-wizard-header-step-text">
@if (! $step->isLabelHidden())
<span class="fi-sc-wizard-header-step-label">
{{ $step->getLabel() }}
</span>
@endif
@if (! $loop->last)
<svg
fill="none"
preserveAspectRatio="none"
viewBox="0 0 22 80"
aria-hidden="true"
class="fi-sc-wizard-header-step-separator"
>
<path
d="M0 -2L20 40L0 82"
stroke-linejoin="round"
stroke="currentcolor"
vector-effect="non-scaling-stroke"
></path>
</svg>
@endif
</li>
@endforeach
</ol>
@if (filled($description = $step->getDescription()))
<span
class="fi-sc-wizard-header-step-description"
>
{{ $description }}
</span>
@endif
</div>
</button>
@foreach ($getChildSchema()->getComponents() as $step)
@if (! $loop->last)
<svg
fill="none"
preserveAspectRatio="none"
viewBox="0 0 22 80"
aria-hidden="true"
class="fi-sc-wizard-header-step-separator"
>
<path
d="M0 -2L20 40L0 82"
stroke-linejoin="round"
stroke="currentcolor"
vector-effect="non-scaling-stroke"
></path>
</svg>
@endif
</li>
@endforeach
</ol>
@endif
@foreach ($steps as $step)
{{ $step }}
@endforeach
@@ -36,6 +36,8 @@ class Wizard extends Component
protected string | Closure | null $alpineSubmitHandler = null;
protected bool | Closure $isHeaderHidden = false;
/**
* @var view-string
*/
@@ -285,4 +287,16 @@ class Wizard extends Component
{
return $this->evaluate($this->alpineSubmitHandler);
}
public function hiddenHeader(bool | Closure $condition = true): static
{
$this->isHeaderHidden = $condition;
return $this;
}
public function isHeaderHidden(): bool
{
return (bool) $this->evaluate($this->isHeaderHidden);
}
}