Add 2FA enable confirmation using TOTP code

@package laravel/jetstream

--- src/Http/Livewire/TwoFactorAuthenticationForm.php
+++ src/Http/Livewire/TwoFactorAuthenticationForm.php
@@ -3,8 +3,9 @@
 namespace Laravel\Jetstream\Http\Livewire;

 use Illuminate\Support\Facades\Auth;
+use Laravel\Fortify\Actions\ConfirmEnableTwoFactorAuthentication;
 use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
-use Laravel\Fortify\Actions\EnableTwoFactorAuthentication;
+use Laravel\Fortify\Actions\GenerateTwoFactorAuthenticationSecret;
 use Laravel\Fortify\Actions\GenerateNewRecoveryCodes;
 use Laravel\Fortify\Features;
 use Laravel\Jetstream\ConfirmsPasswords;
@@ -29,23 +30,47 @@ class TwoFactorAuthenticationForm extends Component
     public $showingRecoveryCodes = false;

     /**
-     * Enable two factor authentication for the user.
+     * Code to confirm activation of two-factor authentication
+     * @var string
+     */
+    public $confirmationCode = null;
+
+    /**
+     * Generate two factor authentication secret for user.
      *
-     * @param  \Laravel\Fortify\Actions\EnableTwoFactorAuthentication  $enable
+     * @param  \Laravel\Fortify\Actions\GenerateTwoFactorAuthenticationSecret  $generate
      * @return void
      */
-    public function enableTwoFactorAuthentication(EnableTwoFactorAuthentication $enable)
+    public function generateTwoFactorAuthenticationSecret(GenerateTwoFactorAuthenticationSecret $generate)
     {
         if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')) {
             $this->ensurePasswordIsConfirmed();
         }

-        $enable(Auth::user());
+        $generate(Auth::user());

         $this->showingQrCode = true;
         $this->showingRecoveryCodes = true;
     }

+    /**
+     * Enable two factor authentication for the user.
+     *
+     * @param  \Laravel\Fortify\Actions\ConfirmEnableTwoFactorAuthentication  $enable
+     * @return void
+     */
+    public function confirmEnableTwoFactorAuthentication(ConfirmEnableTwoFactorAuthentication $enable)
+    {
+        $enabled = $enable(Auth::user(), $this->confirmationCode);
+
+        if (!$enabled) {
+            $this->addError('confirmationCode', __('The provided two factor authentication code was invalid.'));
+            return;
+        }
+
+        $this->hideSetup();
+    }
+
     /**
      * Display the user's recovery codes.
      *
@@ -89,9 +114,17 @@ public function disableTwoFactorAuthentication(DisableTwoFactorAuthentication $d
             $this->ensurePasswordIsConfirmed();
         }

+        $this->hideSetup();
         $disable(Auth::user());
     }

+    protected function hideSetup()
+    {
+        $this->showingQrCode = false;
+        $this->showingRecoveryCodes = false;
+        $this->confirmationCode = null;
+    }
+
     /**
      * Get the current user of the application.
      *
@@ -102,6 +135,16 @@ public function getUserProperty()
         return Auth::user();
     }

+    /**
+     * Determine if two-factor authentication is pending configuration.
+     *
+     * @return bool
+     */
+    public function getSetupProperty()
+    {
+        return ! empty($this->user->two_factor_secret) && !$this->user->is_two_factor_enabled;
+    }
+
     /**
      * Determine if two factor authentication is enabled.
      *
@@ -109,7 +152,7 @@ public function getUserProperty()
      */
     public function getEnabledProperty()
     {
-        return ! empty($this->user->two_factor_secret);
+        return ! empty($this->user->two_factor_secret) && $this->user->is_two_factor_enabled;
     }

     /**

--- src/Http/Middleware/ShareInertiaData.php
+++ src/Http/Middleware/ShareInertiaData.php
@@ -48,7 +48,8 @@ public function handle($request, $next)
                 return array_merge($request->user()->toArray(), array_filter([
                     'all_teams' => Jetstream::hasTeamFeatures() ? $request->user()->allTeams() : null,
                 ]), [
-                    'two_factor_enabled' => ! is_null($request->user()->two_factor_secret),
+                    'two_factor_enabled' => $request->user()->is_two_factor_enabled,
+                    'two_factor_setup' => ! is_null($request->user()->two_factor_secret),
                 ]);
             },
             'errorBags' => function () {

--- stubs/inertia/resources/js/Pages/Profile/TwoFactorAuthenticationForm.vue
+++ stubs/inertia/resources/js/Pages/Profile/TwoFactorAuthenticationForm.vue
@@ -23,37 +23,44 @@
                 </p>
             </div>

-            <div v-if="twoFactorEnabled">
-                <div v-if="qrCode">
-                    <div class="mt-4 max-w-xl text-sm text-gray-600">
-                        <p class="font-semibold">
-                            Two factor authentication is now enabled. Scan the following QR code using your phone's authenticator application.
-                        </p>
-                    </div>
+            <div v-if="recoveryCodes.length > 0">
+                <div class="mt-4 max-w-xl text-sm text-gray-600">
+                    <p class="font-semibold">
+                        Store these recovery codes in a secure password manager. They can be used to recover access to your account if your two factor authentication device is lost.
+                    </p>
+                </div>

-                    <div class="mt-4" v-html="qrCode">
+                <div class="grid gap-1 max-w-xl mt-4 px-4 py-4 font-mono text-sm bg-gray-100 rounded-lg">
+                    <div v-for="code in recoveryCodes" :key="code">
+                        {{ code }}
                     </div>
                 </div>
+            </div>

-                <div v-if="recoveryCodes.length > 0">
-                    <div class="mt-4 max-w-xl text-sm text-gray-600">
-                        <p class="font-semibold">
-                            Store these recovery codes in a secure password manager. They can be used to recover access to your account if your two factor authentication device is lost.
-                        </p>
-                    </div>
+            <div v-if="twoFactorSetup || displayQrCord">
+                <div class="mt-4 max-w-xl text-sm text-gray-600">
+                    <p class="font-semibold">
+                        Scan the following QR code using your phone\'s authenticator application to setup two factor authentication.
+                    </p>
+                </div>

-                    <div class="grid gap-1 max-w-xl mt-4 px-4 py-4 font-mono text-sm bg-gray-100 rounded-lg">
-                        <div v-for="code in recoveryCodes" :key="code">
-                            {{ code }}
-                        </div>
+                <div class="mt-4" v-html="qrCode">
+                </div>
+            </div>
+
+            <div v-if="twoFactorSetup">
+                <div class="mt-4 max-w-xl text-sm text-gray-600">
+                    <div class="col-span-6 sm:col-span-4">
+                        <jet-label for="confirmationCode" value="After configuring the authenticator application, enter the code to validate the two-factor authentication." />
+                        <jet-input id="confirmationCode" type="text" class="mt-1 block w-full" v-model="confirmationCode" />
                     </div>
                 </div>
             </div>

             <div class="mt-5">
-                <div v-if="! twoFactorEnabled">
-                    <jet-confirms-password @confirmed="enableTwoFactorAuthentication">
-                        <jet-button type="button" :class="{ 'opacity-25': enabling }" :disabled="enabling">
+                <div v-if="! twoFactorEnabled && ! twoFactorSetup">
+                    <jet-confirms-password @confirmed="setupTwoFactorAuthentication">
+                        <jet-button type="button" :class="{ 'opacity-25': setuping }" :disabled="setuping">
                             Enable
                         </jet-button>
                     </jet-confirms-password>
@@ -73,13 +80,22 @@
                         </jet-secondary-button>
                     </jet-confirms-password>

-                    <jet-confirms-password @confirmed="disableTwoFactorAuthentication">
-                        <jet-danger-button
-                                        :class="{ 'opacity-25': disabling }"
-                                        :disabled="disabling">
-                            Disable
-                        </jet-danger-button>
-                    </jet-confirms-password>
+                    <div v-if="twoFactorEnabled">
+                        <jet-button
+                                        :class="{ 'opacity-25': enabling }"
+                                        :disabled="enabling"
+                                        @click="enableTwoFactorAuthentication">
+                            Confirm
+                        </jet-button>
+                    <div v-else>
+                        <jet-confirms-password @confirmed="disableTwoFactorAuthentication">
+                            <jet-danger-button
+                                            :class="{ 'opacity-25': disabling }"
+                                            :disabled="disabling">
+                                Disable
+                            </jet-danger-button>
+                        </jet-confirms-password>
+                    <div>
                 </div>
             </div>
         </template>
@@ -105,16 +121,18 @@
         data() {
             return {
                 enabling: false,
+                setuping: false,
                 disabling: false,

                 qrCode: null,
                 recoveryCodes: [],
+                confirmationCode: null,
             }
         },

         methods: {
-            enableTwoFactorAuthentication() {
-                this.enabling = true
+            setupTwoFactorAuthentication() {
+                this.setuping = true

                 this.$inertia.post('/user/two-factor-authentication', {}, {
                     preserveScroll: true,
@@ -122,6 +140,19 @@
                         this.showQrCode(),
                         this.showRecoveryCodes(),
                     ]),
+                    onFinish: () => (this.setuping = false),
+                })
+            },
+            enableTwoFactorAuthentication() {
+                this.setuping = false
+                this.enabling = true
+
+                this.$inertia.post('/user/two-factor-authentication/confirm', {code: this.confirmationCode}, {
+                    preserveScroll: true,
+                    onSuccess: () => {
+                        this.qrCode = null;
+                        this.recoveryCodes = [];
+                    },
                     onFinish: () => (this.enabling = false),
                 })
             },
@@ -160,6 +191,9 @@
         computed: {
             twoFactorEnabled() {
                 return ! this.enabling && this.$page.props.user.two_factor_enabled
+            },
+            twoFactorSetup() {
+                return this.setuping || this.$page.props.user.two_factor_setup
             }
         }
     }

--- stubs/livewire/resources/views/profile/two-factor-authentication-form.blade.php
+++ stubs/livewire/resources/views/profile/two-factor-authentication-form.blade.php
@@ -22,37 +22,45 @@
             </p>
         </div>

-        @if ($this->enabled)
-            @if ($showingQrCode)
-                <div class="mt-4 max-w-xl text-sm text-gray-600">
-                    <p class="font-semibold">
-                        {{ __('Two factor authentication is now enabled. Scan the following QR code using your phone\'s authenticator application.') }}
-                    </p>
-                </div>
+        @if ($showingRecoveryCodes)
+            <div class="mt-4 max-w-xl text-sm text-gray-600">
+                <p class="font-semibold">
+                    {{ __('Store these recovery codes in a secure password manager. They can be used to recover access to your account if your two factor authentication device is lost.') }}
+                </p>
+            </div>

-                <div class="mt-4">
-                    {!! $this->user->twoFactorQrCodeSvg() !!}
-                </div>
-            @endif
+            <div class="grid gap-1 max-w-xl mt-4 px-4 py-4 font-mono text-sm bg-gray-100 rounded-lg">
+                @foreach (json_decode(decrypt($this->user->two_factor_recovery_codes), true) as $code)
+                    <div>{{ $code }}</div>
+                @endforeach
+            </div>
+        @endif

-            @if ($showingRecoveryCodes)
-                <div class="mt-4 max-w-xl text-sm text-gray-600">
-                    <p class="font-semibold">
-                        {{ __('Store these recovery codes in a secure password manager. They can be used to recover access to your account if your two factor authentication device is lost.') }}
-                    </p>
-                </div>
+        @if ($this->setup || $showingQrCode)
+            <div class="mt-4 max-w-xl text-sm text-gray-600">
+                <p class="font-semibold">
+                    {{ __('Scan the following QR code using your phone\'s authenticator application to setup two factor authentication.') }}
+                </p>
+            </div>
+
+            <div class="mt-4 dark:p-4 dark:w-56 dark:bg-white">
+                {!! $this->user->twoFactorQrCodeSvg() !!}
+            </div>
+        @endif

-                <div class="grid gap-1 max-w-xl mt-4 px-4 py-4 font-mono text-sm bg-gray-100 rounded-lg">
-                    @foreach (json_decode(decrypt($this->user->two_factor_recovery_codes), true) as $code)
-                        <div>{{ $code }}</div>
-                    @endforeach
+        @if ($this->setup)
+            <div class="mt-4 max-w-xl text-sm text-gray-600">
+                <div class="col-span-6 sm:col-span-4">
+                    <x-jet-label for="confirmationCode" value="{{ __('After configuring the authenticator application, enter the code to validate the two-factor authentication.') }}" />
+                    <x-jet-input id="confirmationCode" type="text" class="mt-1 block w-full" wire:model.defer="confirmationCode" />
+                    <x-jet-input-error for="confirmationCode" class="mt-2" />
                 </div>
-            @endif
+            </div>
         @endif

         <div class="mt-5">
-            @if (! $this->enabled)
-                <x-jet-confirms-password wire:then="enableTwoFactorAuthentication">
+            @if (! $this->setup && !$this->enabled)
+                <x-jet-confirms-password wire:then="generateTwoFactorAuthenticationSecret">
                     <x-jet-button type="button" wire:loading.attr="disabled">
                         {{ __('Enable') }}
                     </x-jet-button>
@@ -72,11 +80,17 @@
                     </x-jet-confirms-password>
                 @endif

-                <x-jet-confirms-password wire:then="disableTwoFactorAuthentication">
-                    <x-jet-danger-button wire:loading.attr="disabled">
-                        {{ __('Disable') }}
-                    </x-jet-danger-button>
-                </x-jet-confirms-password>
+                @if($this->enabled)
+                    <x-jet-confirms-password wire:then="disableTwoFactorAuthentication">
+                        <x-jet-danger-button wire:loading.attr="disabled">
+                            {{ __('Disable') }}
+                        </x-jet-danger-button>
+                    </x-jet-confirms-password>
+                @else
+                    <x-jet-button wire:click="confirmEnableTwoFactorAuthentication" wire:loading.attr="disabled">
+                        {{ __('Confirm') }}
+                    </x-jet-button>
+                @endif
             @endif
         </div>
     </x-slot>