user(); return $this->frame('account.settings.info', [ 'title' => __('Account info'), 'sub' => __('Update your personal information and sign-in email.'), 'user' => $user, ]); } /** * save info pane */ public function infoStore(AccountUpdateRequest $request): RedirectResponse { $user = $request->user(); $user->fill($request->validated()); if ($user->isDirty('email')) { $user->email_verified_at = null; } $user->save(); return Redirect::route('account.info') ->with('toast', __('Account updated!')); } /** * addresses pane (home + work) */ public function addressesForm(Request $request) { $user = $request->user(); // try primary of each kind; fall back to the first of that kind $home = $user->addresses()->where('kind', 'home')->where('is_primary', 1)->first() ?? $user->addresses()->where('kind', 'home')->first(); $work = $user->addresses()->where('kind', 'work')->where('is_primary', 1)->first() ?? $user->addresses()->where('kind', 'work')->first(); // if neither is explicitly billing, fall back to "work if present, else home" $billing = ($home && $home->is_billing) ? 'home' : (($work && $work->is_billing) ? 'work' : ($work ? 'work' : 'home')); return $this->frame('account.settings.addresses', [ 'title' => __('account.settings.addresses.title'), 'sub' => __('account.settings.addresses.subtitle'), 'user' => $user, 'home' => $home, 'work' => $work, 'billing' => $billing, ]); } /** * save addresses pane */ public function addressesStore(Request $request, Geocoder $geocoder): RedirectResponse { $user = $request->user(); $data = $request->validate([ 'billing' => ['required', 'in:home,work'], 'home' => ['sometimes', 'array'], 'home.label' => ['nullable', 'string', 'max:100'], 'home.line1' => ['nullable', 'string', 'max:255'], 'home.line2' => ['nullable', 'string', 'max:255'], 'home.city' => ['nullable', 'string', 'max:120'], 'home.state' => ['nullable', 'string', 'max:64'], 'home.postal' => ['nullable', 'string', 'max:32'], 'home.country' => ['nullable', 'string', 'max:64'], 'work' => ['sometimes', 'array'], 'work.label' => ['nullable', 'string', 'max:100'], 'work.line1' => ['nullable', 'string', 'max:255'], 'work.line2' => ['nullable', 'string', 'max:255'], 'work.city' => ['nullable', 'string', 'max:120'], 'work.state' => ['nullable', 'string', 'max:64'], 'work.postal' => ['nullable', 'string', 'max:32'], 'work.country' => ['nullable', 'string', 'max:64'], ]); DB::transaction(function () use ($user, $data, $geocoder) { $saved = []; // kind => UserAddress $save = function (string $kind, array $payload) use ($user, $geocoder, &$saved) { // short-circuit if nothing filled $filled = collect($payload) ->only(['line1', 'line2', 'city', 'state', 'postal', 'country']) ->filter(fn ($v) => filled($v)); if ($filled->isEmpty()) { return; } // upsert a single primary address of this kind $addr = UserAddress::updateOrCreate( ['user_id' => $user->id, 'kind' => $kind, 'is_primary' => 1], [ 'label' => $payload['label'] ?? ucfirst($kind), 'line1' => $payload['line1'] ?? null, 'line2' => $payload['line2'] ?? null, 'city' => $payload['city'] ?? null, 'state' => $payload['state'] ?? null, 'postal' => $payload['postal'] ?? null, 'country' => $payload['country'] ?? null, ] ); $saved[$kind] = $addr; // geocode $singleLine = collect([ $addr->line1, $addr->line2, $addr->city, $addr->state, $addr->postal, $addr->country, ])->filter()->implode(', '); if ($singleLine === '') { return; } $norm = $geocoder->forward($singleLine); if (!$norm) { return; } $loc = Location::findOrCreateNormalized($norm, $singleLine); $addr->location_id = $loc->id; $addr->save(); }; if (isset($data['home'])) { $save('home', $data['home']); } if (isset($data['work'])) { $save('work', $data['work']); } // billing flag: clear all billing flags for this user UserAddress::where('user_id', $user->id)->update(['is_billing' => 0]); // set billing on the chosen kind if we have that address $kind = $data['billing']; // if the chosen kind wasn't saved in this submission (e.g., user didn't fill it), // try to find an existing primary address of that kind. $billingAddr = $saved[$kind] ?? UserAddress::where('user_id', $user->id) ->where('kind', $kind) ->where('is_primary', 1) ->first(); if ($billingAddr) { $billingAddr->is_billing = 1; $billingAddr->save(); } }); return Redirect::route('account.addresses') ->with('toast', __('Addresses updated!')); } /** * password pane */ public function passwordForm(Request $request) { return $this->frame('account.settings.password', [ 'title' => __('Password'), 'sub' => __('Change your password.'), 'user' => $request->user(), ]); } /** * save password pane */ public function passwordStore(Request $request): RedirectResponse { $data = $request->validate([ 'current_password' => ['required', 'current_password'], 'password' => ['required', 'string', 'min:12', 'confirmed'], ]); $user = $request->user(); $user->forceFill([ 'password' => Hash::make($data['password']), ])->save(); return Redirect::route('account.password') ->with('toast', __('Password updated!')); } /** * delete pane */ public function deleteForm(Request $request) { return $this->frame('account.settings.delete', [ 'title' => __('account.settings.delete.title'), 'sub' => __('account.settings.delete.subtitle'), 'user' => $request->user(), ]); } /** * delete confirmation modal/page */ public function deleteConfirm(Request $request) { if ($request->header('HX-Request')) { return view('account.partials.delete-modal'); } // no-js full page return $this->frame('account.settings.delete-confirm', [ 'title' => __('account.settings.delete-confirm.title'), 'sub' => __('account.settings.delete-confirm.subtitle'), 'user' => $request->user(), ]); } /** * delete the user's account */ public function destroy(Request $request) { // validate: on failure // - full page: redirects to confirm page, flashes errors + input + toast // - htmx: returns 422 modal html + HX-Trigger toast $this->validateHtmx($request, [ 'password' => ['required', 'current_password'], ], [ 'bag' => 'userDeletion', 'redirect' => 'account.delete.confirm', // no-js failure lands back on confirm 'htmx_view' => 'account.modals.delete', // modal content to re-render on 422 'toast_type' => 'error', // optional, default is already error // optional: override toast message (default is first validation message) // 'toast_message' => __('Please correct the password and try again.'), // optional: disable toast on validation failures: // 'toast' => false, ]); // passed validation, delete account $user = $request->user(); Auth::logout(); $user->delete(); $request->session()->invalidate(); $request->session()->regenerateToken(); // success: for no-js, redirect home with toast // for htmx, send HX-Redirect + HX-Trigger (handled inside helper) return $this->redirectWithToast( $request, 'dashboard', // or whatever route you want, could be a url('/') helper variant too __('Your account has been deleted.'), 'success' ); } /** * content frame handler */ private function frame(?string $view = null, array $data = []) { return view('account.index', [ 'view' => $view, 'data' => $data, ]); } }