mirror of
https://github.com/filamentphp/filament.git
synced 2026-09-24 15:42:09 +08:00
redesign 2fa setup modal
This commit is contained in:
@@ -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'),
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -4,7 +4,7 @@ return [
|
||||
|
||||
'actions' => [
|
||||
|
||||
'You can also',
|
||||
'Click to',
|
||||
|
||||
'copy' => [
|
||||
'label' => 'copy',
|
||||
|
||||
+84
-85
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user