billingSnapshot($account?->public_id); return view('email.account.wallet', [ 'balanceMinor' => $balanceMinor, 'spentMinor' => (int) ($ledger['spent_minor'] ?? 0), 'creditedMinor' => (int) ($ledger['credited_minor'] ?? 0), 'topupUrl' => $this->topupUrl(), ]); } /** Billing: wallet snapshot + recurring paid-mailbox subscriptions. */ public function billing(): View { $account = ladill_account(); [$balanceMinor, $ledger] = $this->billingSnapshot($account?->public_id); $paidMailboxes = []; $monthlyTotalMinor = 0; try { foreach ($this->mailboxes->forUser((string) $account?->public_id) as $m) { if (! ($m['is_paid'] ?? false)) { continue; } $m['price_minor'] = MailboxPricing::priceMinorFor((int) ($m['quota_mb'] ?? 0)); $m['quota_label'] = MailboxPricing::label((int) ($m['quota_mb'] ?? 0)); $monthlyTotalMinor += $m['price_minor']; $paidMailboxes[] = $m; } } catch (\Throwable $e) { report($e); } return view('email.account.billing', [ 'balanceMinor' => $balanceMinor, 'spentMinor' => (int) ($ledger['spent_minor'] ?? 0), 'creditedMinor' => (int) ($ledger['credited_minor'] ?? 0), 'paidMailboxes' => $paidMailboxes, 'monthlyTotalMinor' => $monthlyTotalMinor, 'topupUrl' => $this->topupUrl(), ]); } public function settings(): View { $account = ladill_account(); $settings = EmailSetting::firstOrNew(['user_id' => $account?->id]); ['linkStatus' => $linkStatus, 'mailboxOptions' => $mailboxOptions, 'showMailboxLinkUi' => $showMailboxLinkUi] = $this->mailboxLinkContext($account); return view('email.account.settings', [ 'account' => $account, 'settings' => $settings, 'linkStatus' => $linkStatus, 'mailboxOptions' => $mailboxOptions, 'showMailboxLinkUi' => $showMailboxLinkUi, ]); } public function linkMailbox(Request $request): RedirectResponse { $account = ladill_account(); $data = $request->validate([ 'mailbox_address' => ['required', 'email', 'max:255'], ]); try { $result = $this->identity->linkMailbox( (string) $account->public_id, $data['mailbox_address'], ); } catch (\Illuminate\Http\Client\RequestException $e) { $message = (string) $e->response?->json('message', ''); if ($message !== '') { return redirect()->route('account.settings')->with('error', $message); } report($e); return redirect()->route('account.settings')->with('error', 'Could not link mailbox. Try again in a moment, or contact support if this keeps happening.'); } catch (\Throwable $e) { report($e); return redirect()->route('account.settings')->with('error', 'Could not link mailbox. Try again in a moment, or contact support if this keeps happening.'); } $request->session()->forget('mailbox_link_banner_dismissed'); $connectUrl = (string) ($result['webmail_connect_url'] ?? ''); if ($connectUrl === '') { $connectUrl = rtrim((string) config('services.ladill_webmail.url', 'https://mail.ladill.com'), '/').'/sso/connect'; } return redirect()->away($connectUrl); } public function unlinkMailbox(Request $request): RedirectResponse { $account = ladill_account(); try { $this->identity->unlinkMailbox((string) $account->public_id); } catch (\Illuminate\Http\Client\RequestException $e) { $message = (string) $e->response?->json('message', ''); if ($message !== '') { return redirect()->route('account.settings')->with('error', $message); } report($e); return redirect()->route('account.settings')->with('error', 'Could not unlink mailbox. Try again in a moment.'); } catch (\Throwable $e) { report($e); return redirect()->route('account.settings')->with('error', 'Could not unlink mailbox. Try again in a moment.'); } $request->session()->forget('mailbox_link_banner_dismissed'); return redirect()->route('account.settings')->with('success', 'Mailbox unlinked from your Ladill account.'); } /** @return array{linkStatus: array, mailboxOptions: array>, showMailboxLinkUi: bool} */ private function mailboxLinkContext(?User $account): array { $linkStatus = ['show_reminder' => false]; $mailboxOptions = []; if (! $account?->public_id) { return [ 'linkStatus' => $linkStatus, 'mailboxOptions' => $mailboxOptions, 'showMailboxLinkUi' => false, ]; } try { $mailboxOptions = $this->mailboxes->forUser((string) $account->public_id); } catch (\Throwable $e) { report($e); } try { $linkStatus = $this->identity->mailboxLinkStatus((string) $account->public_id); } catch (\Throwable $e) { report($e); $linkStatus = $this->localMailboxLinkStatus($account, $mailboxOptions); } $showMailboxLinkUi = (bool) ($linkStatus['linked_mailbox'] ?? null) || ($linkStatus['show_reminder'] ?? false) || $this->localNeedsMailboxLink($account, $mailboxOptions, $linkStatus); return [ 'linkStatus' => $linkStatus, 'mailboxOptions' => $mailboxOptions, 'showMailboxLinkUi' => $showMailboxLinkUi, ]; } /** @param array> $mailboxOptions */ private function localNeedsMailboxLink(User $account, array $mailboxOptions, array $linkStatus): bool { if ($linkStatus['linked_mailbox'] ?? null) { return false; } $email = strtolower(trim($account->email ?? '')); if ($email === '') { return false; } foreach ($mailboxOptions as $mailbox) { if (strtolower((string) ($mailbox['address'] ?? '')) === $email) { return false; } } return count($mailboxOptions) > 0; } /** @param array> $mailboxOptions */ /** @return array */ private function localMailboxLinkStatus(User $account, array $mailboxOptions): array { $needsLink = $this->localNeedsMailboxLink($account, $mailboxOptions, []); return [ 'show_reminder' => $needsLink, 'stage' => count($mailboxOptions) > 0 ? 'needs_link' : 'needs_mailbox', 'linked_mailbox' => null, 'account_email' => $account->email, ]; } public function updateSettings(Request $request): RedirectResponse { $account = ladill_account(); $data = $request->validate([ 'default_quota_mb' => ['nullable', 'integer', 'min:256', 'max:153600'], 'notify_email' => ['nullable', 'email', 'max:255'], 'product_updates' => ['nullable', 'boolean'], ]); EmailSetting::updateOrCreate( ['user_id' => $account->id], [ 'default_quota_mb' => $data['default_quota_mb'] ?? null, 'notify_email' => $data['notify_email'] ?? null, 'product_updates' => $request->boolean('product_updates'), ], ); return redirect()->route('account.settings')->with('success', 'Settings saved.'); } /** * @return array{0:int,1:array} [balanceMinor, ledger] */ private function billingSnapshot(?string $publicId): array { if (! $publicId) { return [0, []]; } try { return [ $this->wallet->balanceMinor(ladill_account()), $this->billing->serviceLedger($publicId, (string) config('billing.service', 'email')), ]; } catch (\Throwable $e) { report($e); return [0, []]; } } }