diff --git a/app/Http/Controllers/Account/AccountController.php b/app/Http/Controllers/Account/AccountController.php index 8467aac..bade0e6 100644 --- a/app/Http/Controllers/Account/AccountController.php +++ b/app/Http/Controllers/Account/AccountController.php @@ -10,6 +10,8 @@ use Illuminate\View\View; use Lunar\Models\Country; use Lunar\Models\State; +use Modules\Core\Cart\Services\CartService; +use Modules\Core\Checkout\Services\CheckoutService; use Modules\Core\Customer\Services\CustomerAccountService; use Modules\Core\Privacy\Services\PrivacyService; @@ -68,6 +70,7 @@ public function update(string $locale, Request $request): RedirectResponse Rule::exists((new State)->getTable(), 'name')->where('country_id', $country->id), ], 'contact_phone' => ['nullable', 'string', 'max:30'], + 'recovery_consent' => ['boolean'], ]); $invoice = $request->boolean('invoice'); @@ -100,9 +103,37 @@ public function update(string $locale, Request $request): RedirectResponse : $this->account->createAddress($user, $addressData); } + $this->updateRecoveryConsent($customer, $request->boolean('recovery_consent')); + return redirect()->route('account')->with('status', __('storefront.account.saved')); } + /** + * "Email me a reminder if I don't finish my order", as a standing choice. + * Stored on the customer in the same meta shape the checkout writes (see + * CheckoutController::rememberRecoveryConsent()), and applied to the + * current cart too, so opting out stops reminders for it right away. + */ + private function updateRecoveryConsent($customer, bool $consent): void + { + if ((bool) data_get($customer, 'meta.recovery_consent') !== $consent) { + $customer->meta = [ + ...($customer->meta?->toArray() ?? []), + 'recovery_consent' => $consent, + 'recovery_consent_at' => $consent ? now()->toIso8601String() : null, + 'recovery_consent_policy_version' => $consent ? config('legal.privacy_policy_version') : null, + ]; + $customer->save(); + } + + // Only an existing cart; never create one just to record this. + $cart = app(CartService::class)->current(); + + if ($cart && (bool) data_get($cart, 'meta.recovery_consent') !== $consent) { + app(CheckoutService::class)->setRecoveryConsent($consent); + } + } + /** * Self-service deletion: opens core's 30-day grace-period erasure request * (which blocks the login right away) and logs out. Logging back in within diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 3517283..04420d8 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -25,8 +25,19 @@ */ class LoginController extends Controller { - public function create(string $locale): View + /** + * `?redirect=/el/checkout` (e.g. from the checkout's login tab) becomes the + * intended URL that verify() returns to. Only a same-site path is accepted: + * no scheme, no protocol-relative `//host`, so it can't redirect off-site. + */ + public function create(string $locale, Request $request): View { + $redirect = (string) $request->query('redirect', ''); + + if (preg_match('#^/(?![/\\\\])#', $redirect)) { + $request->session()->put('url.intended', url($redirect)); + } + return view('auth.login'); } @@ -38,6 +49,9 @@ public function send(string $locale, Request $request, UserOtpService $otp): Red $email = Str::lower(trim($validated['email'])); + $userModel = config('auth.providers.users.model'); + $isNewAccount = ! $userModel::where('email', $email)->exists(); + try { $otp->generateAndSend($email); } catch (OtpThrottledException) { @@ -46,6 +60,16 @@ public function send(string $locale, Request $request, UserOtpService $otp): Red ]); } + // generateAndSend() just created the account: record that it happened + // under the login page's terms notice, and which versions it showed. + if ($isNewAccount) { + $userModel::where('email', $email)->update([ + 'terms_accepted_at' => now(), + 'terms_version' => config('legal.terms_version'), + 'privacy_policy_version' => config('legal.privacy_policy_version'), + ]); + } + $request->session()->put('login.email', $email); return redirect()->route('login.code'); diff --git a/app/Http/Controllers/Checkout/CheckoutController.php b/app/Http/Controllers/Checkout/CheckoutController.php index c943ce0..46c3375 100644 --- a/app/Http/Controllers/Checkout/CheckoutController.php +++ b/app/Http/Controllers/Checkout/CheckoutController.php @@ -7,6 +7,7 @@ use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; use Illuminate\View\View; @@ -22,6 +23,7 @@ use Modules\Core\Checkout\Exceptions\TermsNotAcceptedException; use Modules\Core\Checkout\Exceptions\UnknownPaymentTypeException; use Modules\Core\Checkout\Services\CheckoutService; +use Modules\Core\Customer\Services\CustomerAccountService; use Modules\Core\Payment\Enums\PaymentResultStatus; use Modules\Core\Payment\Models\PaymentMethod; @@ -38,7 +40,11 @@ * lenient — nothing is rejected mid-typing; required-field enforcement is the * job of the (not-yet-built) "Continue to payment" gate. * - * Guest-only for now — the Contact section's login tab is UI only. + * Guests type their email; the login tab links to the storefront's own + * `login` route and back. Logged in: the email is the account's (forced in + * saveAddress()), the first visit prefills addresses from the account + * (prefillFromAccount()), and Lunar's Login listener has already attached the + * cart, so the placed order lands in the account's history. * * Single-country store: STORE_COUNTRY_ISO3 fixes the country to Greece (hidden * field, forced server-side). Set it to null for the full country picker (the @@ -51,6 +57,7 @@ class CheckoutController extends Controller public function __construct( private readonly CartService $cart, private readonly CheckoutService $checkout, + private readonly CustomerAccountService $account, ) {} public function show(string $locale): View @@ -61,10 +68,24 @@ public function show(string $locale): View $shippingOptions = collect(); + // Captured before prefillFromAccount(), which may recreate the address + // row (dropping its shipping_option) — same reason as in saveAddress(). + $previousOption = $cart?->shippingAddress?->shipping_option; + + if ($cart && Auth::check()) { + $cart = $this->prefillFromAccount($cart); + + // Nothing chosen on this cart yet: carry over the account's standing + // opt-in (an explicit earlier choice, recorded with its own + // timestamp/policy version). Never opts anyone in by default. + if (! array_key_exists('recovery_consent', $cart->meta?->toArray() ?? []) + && data_get($this->account->customer(Auth::user()), 'meta.recovery_consent')) { + $cart = $this->checkout->setRecoveryConsent(true); + } + } + if ($cart?->shippingAddress) { - // Nothing recreates the address row in this path — its own current - // value is the correct "previous" to carry forward if still valid. - $shippingOptions = $this->syncShipping($cart, $cart->shippingAddress->shipping_option); + $shippingOptions = $this->syncShipping($cart, $previousOption); // Cart's CachesProperties::refresh() explicitly nulls total/ // subTotal/shippingTotal/etc. back to their defaults — every @@ -96,6 +117,7 @@ public function show(string $locale): View 'shippingOptions' => $shippingOptions, 'paymentMethods' => $paymentMethods, 'shipToBilling' => (bool) data_get($cart, 'meta.ship_to_billing', true), + 'wantsInvoice' => (bool) data_get($cart, 'meta.wants_invoice', false), 'storeCountry' => $storeCountry, 'countries' => $storeCountry ? collect() @@ -147,7 +169,6 @@ public function saveAddress(string $locale, Request $request): JsonResponse 'shipping_first_name' => ['nullable', 'string', 'max:255'], 'shipping_last_name' => ['nullable', 'string', 'max:255'], - 'shipping_company_name' => ['nullable', 'string', 'max:255'], 'shipping_line_one' => ['nullable', 'string', 'max:255'], 'shipping_city' => ['nullable', 'string', 'max:255'], 'shipping_state' => $stateRule, @@ -160,14 +181,24 @@ public function saveAddress(string $locale, Request $request): JsonResponse $errors = $validator->errors()->toArray(); $data = $validator->valid(); + // Logged in: the order email is always the account's. It isn't a field + // on the page then, and a submitted value isn't trusted. + if ($user = Auth::user()) { + $data['contact_email'] = $user->email; + } + $billingCountryId = $storeCountry?->id ?? ($data['billing_country_id'] ?? null); $shippingCountryId = $storeCountry?->id ?? ($data['shipping_country_id'] ?? $billingCountryId); + // Company/ΑΦΜ only count when "I want an invoice" is ticked; the fields + // stay in the DOM (just hidden) when it isn't, so ignore what they send. + $wantsInvoice = $request->boolean('wants_invoice'); + $billing = [ 'first_name' => $data['billing_first_name'] ?? null, 'last_name' => $data['billing_last_name'] ?? null, - 'company_name' => $data['billing_company_name'] ?? null, - 'tax_identifier' => $data['billing_tax_identifier'] ?? null, + 'company_name' => $wantsInvoice ? ($data['billing_company_name'] ?? null) : null, + 'tax_identifier' => $wantsInvoice ? ($data['billing_tax_identifier'] ?? null) : null, 'line_one' => $data['billing_line_one'] ?? null, 'city' => $data['billing_city'] ?? null, 'state' => $data['billing_state'] ?? null, @@ -178,11 +209,13 @@ public function saveAddress(string $locale, Request $request): JsonResponse ]; $shipping = $sameAsBilling - ? [...$billing, 'delivery_instructions' => $data['shipping_delivery_instructions'] ?? null] + ? [ + ...array_diff_key($billing, ['company_name' => 1, 'tax_identifier' => 1]), + 'delivery_instructions' => $data['shipping_delivery_instructions'] ?? null, + ] : [ 'first_name' => $data['shipping_first_name'] ?? null, 'last_name' => $data['shipping_last_name'] ?? null, - 'company_name' => $data['shipping_company_name'] ?? null, 'line_one' => $data['shipping_line_one'] ?? null, 'city' => $data['shipping_city'] ?? null, 'state' => $data['shipping_state'] ?? null, @@ -196,7 +229,11 @@ public function saveAddress(string $locale, Request $request): JsonResponse $this->checkout->setBillingAddress($billing); $cart = $this->checkout->setShippingAddress($shipping); - $cart->meta = [...($cart->meta?->toArray() ?? []), 'ship_to_billing' => $sameAsBilling]; + $cart->meta = [ + ...($cart->meta?->toArray() ?? []), + 'ship_to_billing' => $sameAsBilling, + 'wants_invoice' => $wantsInvoice, + ]; $cart->save(); // Abandoned-cart-recovery opt-in — boboko-core owns the record (bool + @@ -204,6 +241,10 @@ public function saveAddress(string $locale, Request $request): JsonResponse // Deliberately its own scope, not merged with any future newsletter opt-in. $this->checkout->setRecoveryConsent($request->boolean('recovery_consent')); + if (Auth::check()) { + $this->rememberRecoveryConsent($request->boolean('recovery_consent')); + } + $rateKeyAfter = $cart->shippingAddress?->only(['postcode', 'state', 'country_id']); $rateChanged = $rateKeyAfter != $rateKeyBefore; @@ -310,6 +351,15 @@ public function placeOrder(string $locale, Request $request): JsonResponse ], 422); } + // Lenient autosave never requires these; this is the gate. + if (data_get($cart, 'meta.wants_invoice') + && (blank($cart->billingAddress?->company_name) || blank($cart->billingAddress?->tax_identifier))) { + return response()->json([ + 'status' => 'invalid', + 'message' => __('checkout.page.invoice_required'), + ], 422); + } + // Same check Lunar's own ValidateCartForOrderCreation runs inside // initiatePayment() (a product unpublished/deleted after it was // added to the cart) — checked here first so the shopper is told @@ -528,6 +578,116 @@ private function fragments(?Cart $cart, ?Collection $options, array $errors = [] ]); } + /** + * Logged-in shopper: fills any BLANK cart address field from the account + * (name, saved default address, phone, email), on every checkout load, so + * an account filled in after checkout started still shows up. Never + * overwrites anything already in the cart. + * + * Company/ΑΦΜ (and ticking "I want an invoice") only on the first pass + * (meta.account_prefilled): someone who then clears them or unticks the + * box for this order shouldn't get them back on the next reload. + * + * Writes only when something actually changes, so a normal reload costs + * nothing extra. + */ + private function prefillFromAccount(Cart $cart): Cart + { + $user = Auth::user(); + $customer = $this->account->customer($user); + $addresses = collect($this->account->addresses($user)); + $saved = $addresses->firstWhere('shipping_default', true) ?? $addresses->first(); + $firstPass = ! data_get($cart, 'meta.account_prefilled'); + + $fromAccount = array_filter([ + 'first_name' => $customer?->first_name ?: $saved?->first_name, + 'last_name' => $customer?->last_name ?: $saved?->last_name, + 'line_one' => $saved?->line_one, + 'city' => $saved?->city, + 'state' => $saved?->state, + 'postcode' => $saved?->postcode, + 'country_id' => $this->storeCountry()?->id ?? $saved?->country_id, + 'contact_email' => $user->email, + 'contact_phone' => $saved?->contact_phone, + ], 'filled'); + + $invoice = $firstPass + ? array_filter([ + 'company_name' => $customer?->company_name, + 'tax_identifier' => $customer?->tax_identifier, + ], 'filled') + : []; + + $fields = ['first_name', 'last_name', 'company_name', 'tax_identifier', 'line_one', 'city', + 'state', 'postcode', 'country_id', 'contact_email', 'contact_phone']; + + $fillBlanks = function (?array $current, array $values) { + $current ??= []; + + foreach ($values as $key => $value) { + if (blank($current[$key] ?? null)) { + $current[$key] = $value; + } + } + + return $current; + }; + + $billingBefore = $cart->billingAddress?->only($fields); + $billing = $fillBlanks($billingBefore, [...$fromAccount, ...$invoice]); + + // Shipping has no company/ΑΦΜ (same shape saveAddress() writes). + $shipToBilling = (bool) data_get($cart, 'meta.ship_to_billing', true); + $shippingFields = [...array_diff($fields, ['company_name', 'tax_identifier']), 'delivery_instructions']; + + $shippingBefore = $cart->shippingAddress?->only($shippingFields); + $shipping = $shipToBilling + ? [ + ...array_diff_key($billing, ['company_name' => 1, 'tax_identifier' => 1]), + 'delivery_instructions' => $shippingBefore['delivery_instructions'] ?? null, + ] + : $fillBlanks($shippingBefore, $fromAccount); + + if ($billing != ($billingBefore ?? []) || $shipping != ($shippingBefore ?? [])) { + $this->checkout->setBillingAddress($billing); + $cart = $this->checkout->setShippingAddress($shipping); + } + + if ($firstPass) { + $cart->meta = [ + ...($cart->meta?->toArray() ?? []), + 'account_prefilled' => true, + 'wants_invoice' => (bool) data_get($cart, 'meta.wants_invoice') || $invoice !== [], + ]; + $cart->save(); + } + + return $cart; + } + + /** + * The shopper's latest reminder choice, kept on their customer record + * (meta, same shape CheckoutService::setRecoveryConsent() writes on the + * cart) so their next checkout starts from it. The storefront's account + * page reads/writes the same keys. Candidate for a boboko-core method. + */ + private function rememberRecoveryConsent(bool $consent): void + { + $customer = $this->account->customer(Auth::user()); + + if (! $customer || (bool) data_get($customer, 'meta.recovery_consent') === $consent) { + return; + } + + $customer->meta = [ + ...($customer->meta?->toArray() ?? []), + 'recovery_consent' => $consent, + 'recovery_consent_at' => $consent ? now()->toIso8601String() : null, + 'recovery_consent_policy_version' => $consent ? config('legal.privacy_policy_version') : null, + ]; + $customer->save(); + } + private function storeCountry(): ?Country { if (self::STORE_COUNTRY_ISO3 === null) { diff --git a/app/Listeners/SaveAddressFromFirstOrder.php b/app/Listeners/SaveAddressFromFirstOrder.php new file mode 100644 index 0000000..75b6b3a --- /dev/null +++ b/app/Listeners/SaveAddressFromFirstOrder.php @@ -0,0 +1,57 @@ +order; + $user = $order->user; + $shipping = $order->shippingAddress; + + if (! $user || ! $shipping || ! $shipping->line_one) { + return; + } + + $customer = $this->account->customer($user); + + if (! $customer) { + return; + } + + if (! $customer->first_name && ! $customer->last_name) { + $this->account->updateProfile($user, [ + 'first_name' => $shipping->first_name, + 'last_name' => $shipping->last_name, + ]); + } + + if (collect($this->account->addresses($user))->isNotEmpty()) { + return; + } + + $this->account->createAddress($user, [ + ...$shipping->only(['first_name', 'last_name', 'line_one', 'city', 'state', 'postcode', 'country_id', 'contact_phone']), + 'contact_email' => $user->email, + 'shipping_default' => true, + 'billing_default' => true, + ]); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index f2d1d11..7fd65fe 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -46,6 +46,7 @@ protected function casts(): array { return [ 'email_verified_at' => 'datetime', + 'terms_accepted_at' => 'datetime', ]; } } diff --git a/composer.lock b/composer.lock index 700f0f5..e035eaa 100644 --- a/composer.lock +++ b/composer.lock @@ -935,16 +935,16 @@ }, { "name": "composer/semver", - "version": "3.4.4", + "version": "3.5.0", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" + "reference": "f7a296f4c4cf8cb8bb83e35d6951a406bb11afa5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", - "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "url": "https://api.github.com/repos/composer/semver/zipball/f7a296f4c4cf8cb8bb83e35d6951a406bb11afa5", + "reference": "f7a296f4c4cf8cb8bb83e35d6951a406bb11afa5", "shasum": "" }, "require": { @@ -986,7 +986,7 @@ "homepage": "http://robbast.nl" } ], - "description": "Semver library that offers utilities, version constraint parsing and validation.", + "description": "Version comparison library that offers utilities, version constraint parsing and validation.", "keywords": [ "semantic", "semver", @@ -996,7 +996,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.4" + "source": "https://github.com/composer/semver/tree/3.5.0" }, "funding": [ { @@ -1008,7 +1008,7 @@ "type": "github" } ], - "time": "2025-08-20T19:15:30+00:00" + "time": "2026-09-24T14:38:51+00:00" }, { "name": "danharrin/date-format-converter", diff --git a/config/checkout.php b/config/checkout.php new file mode 100644 index 0000000..9758154 --- /dev/null +++ b/config/checkout.php @@ -0,0 +1,18 @@ +`, so the + * login page must send the shopper back there afterwards (3dealer's + * Auth\LoginController does). null: no login offered in checkout at all. + */ + 'login_route' => 'login', + +]; diff --git a/database/migrations/2026_09_24_000002_add_terms_acceptance_to_users_table.php b/database/migrations/2026_09_24_000002_add_terms_acceptance_to_users_table.php new file mode 100644 index 0000000..55eb641 --- /dev/null +++ b/database/migrations/2026_09_24_000002_add_terms_acceptance_to_users_table.php @@ -0,0 +1,29 @@ +timestamp('terms_accepted_at')->nullable(); + $table->string('terms_version')->nullable(); + $table->string('privacy_policy_version')->nullable(); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn(['terms_accepted_at', 'terms_version', 'privacy_policy_version']); + }); + } +}; diff --git a/resources/css/checkout.css b/resources/css/checkout.css index 444ba98..51a4648 100644 --- a/resources/css/checkout.css +++ b/resources/css/checkout.css @@ -504,35 +504,12 @@ .bbk-checkout-note { font-size: 0.875rem; } -/* Contact tabs */ +/* Contact: "logged in as" line, or the guest login prompt */ -.bbk-checkout-tabs { display: flex; flex-direction: column; gap: 1rem; } +.bbk-checkout-logged-in, +.bbk-checkout-login-prompt { margin: 0; } -.bbk-checkout-tab { - display: inline-flex; - width: fit-content; - margin-right: 0.5rem; - padding: 0.5rem 1rem; - border: 1px solid var(--bbk-color-border); - border-radius: var(--bbk-radius-sm); - background: var(--bbk-color-bg); - font: inherit; - font-weight: 600; - color: var(--bbk-color-muted); - cursor: pointer; - transition: background-color 0.15s ease, color 0.15s ease; -} - -.bbk-checkout-tab[aria-selected="true"] { - background: var(--bbk-color-text); - border-color: var(--bbk-color-text); - color: var(--bbk-color-bg); -} - -.bbk-checkout-tab:focus-visible { - outline: 2px solid var(--bbk-color-accent); - outline-offset: 2px; -} +.bbk-checkout-login-prompt a { color: inherit; font-weight: 600; } /* Fields */ @@ -602,6 +579,12 @@ .bbk-checkbox { } /* For a full-sentence label that can wrap — align the box to the first line. */ +/* "I want an invoice": company/ΑΦΜ only while ticked */ + +.bbk-invoice { display: flex; flex-direction: column; gap: 1rem; } + +.bbk-invoice:not(:has(input[name="wants_invoice"]:checked)) .bbk-invoice-fields { display: none; } + .bbk-checkbox--stacked { display: flex; align-items: flex-start; @@ -686,6 +669,8 @@ .bbk-checkout-continue { font: inherit; font-weight: 600; text-align: center; + text-decoration: none; + box-sizing: border-box; cursor: pointer; transition: opacity 0.15s ease; } @@ -869,8 +854,3 @@ .bbk-address-lines { font-size: 0.875rem; color: var(--bbk-color-muted); } - -.bbk-confirmation-continue { - max-width: 280px; - text-decoration: none; -} diff --git a/resources/js/checkout/bbk-checkout-form-controller.js b/resources/js/checkout/bbk-checkout-form-controller.js index 7dfbe05..a8e720e 100644 --- a/resources/js/checkout/bbk-checkout-form-controller.js +++ b/resources/js/checkout/bbk-checkout-form-controller.js @@ -13,7 +13,6 @@ import { csrfToken } from './csrf' // already uses). Shipping-method radios post to selectShippingUrl the same way. export default class extends Controller { static targets = [ - 'guestTab', 'loginTab', 'guestPanel', 'loginPanel', 'sameAsBilling', 'shippingFields', 'form', 'shippingOptions', 'status', ] @@ -41,22 +40,6 @@ export default class extends Controller { this.saveController?.abort() } - // ── Contact tabs ──────────────────────────────────────────────────── - - showGuest() { - this.guestPanelTarget.hidden = false - this.loginPanelTarget.hidden = true - this.guestTabTarget.setAttribute('aria-selected', 'true') - this.loginTabTarget.setAttribute('aria-selected', 'false') - } - - showLogin() { - this.guestPanelTarget.hidden = true - this.loginPanelTarget.hidden = false - this.guestTabTarget.setAttribute('aria-selected', 'false') - this.loginTabTarget.setAttribute('aria-selected', 'true') - } - // ── Same as billing ──────────────────────────────────────────────── toggleSameAsBilling() { @@ -93,7 +76,6 @@ export default class extends Controller { // react to fields that actually belong to the address form. const el = event.target const belongsToForm = el.form?.id === 'bbk-address-form' - || el.closest('[data-bbk-checkout-form-target="guestPanel"]') if (!belongsToForm) return // No status during the wait — it only shows once the request is in flight, diff --git a/resources/views/account/show.blade.php b/resources/views/account/show.blade.php index 4e8633d..abe75a0 100644 --- a/resources/views/account/show.blade.php +++ b/resources/views/account/show.blade.php @@ -133,6 +133,22 @@ + {{-- Standing opt-in for abandoned-cart reminders (explicit, off by + default); checkout starts from it and can change it again. --}} +
+

{{ __('storefront.account.emails_heading') }}

+ +
+ + +
+
+
{{ __('storefront.account.save') }}
diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index e35a5bf..0a25469 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -42,6 +42,16 @@ /> + {{-- Shown to everyone; for a new account, LoginController::send() + records the time and the terms/privacy versions. Unescaped: the + label holds the two links (Language Lines, staff only). --}} +

+ {!! __('storefront.auth.terms_notice', [ + 'terms' => route('legal.terms'), + 'privacy' => route('legal.privacy'), + ]) !!} +

+
{{ __('storefront.auth.send_code') }}
diff --git a/resources/views/checkout/confirmation.blade.php b/resources/views/checkout/confirmation.blade.php index e5769a6..0c90408 100644 --- a/resources/views/checkout/confirmation.blade.php +++ b/resources/views/checkout/confirmation.blade.php @@ -41,6 +41,17 @@

{{ __('checkout.page.confirmation_email_note') }}

+ {{-- Guests: logging in with the order's email attaches it to an account + (App\Listeners\ClaimGuestOrdersOnLogin), so it shows in their history. --}} + @guest + @if ($loginRoute = config('checkout.login_route')) +

+ {{ __('checkout.page.confirmation_login_hint') }} + {{ __('checkout.page.login_link') }} +

+ @endif + @endguest +
@foreach ($order->lines->where('type', '!=', 'shipping') as $line) @@ -116,9 +127,5 @@ @endif
- - - {{ __('checkout.page.confirmation_continue') }} - @endsection diff --git a/resources/views/checkout/page.blade.php b/resources/views/checkout/page.blade.php index 2e3688a..68a44e6 100644 --- a/resources/views/checkout/page.blade.php +++ b/resources/views/checkout/page.blade.php @@ -38,73 +38,51 @@ class="bbk-checkout-main" data-bbk-payment-processing-slow-value="{{ __('checkout.page.payment_processing_slow') }}" > - {{-- Contact --}} + {{-- Contact. Logged in: the order email is the account's (forced + server-side in saveAddress()), so there's no field. Guests type + their email, plus a login link when config('checkout.login_route') + is set; the storefront's login page sends them back here and Lunar + merges the guest cart into the account. --}} + @php($loginRoute = config('checkout.login_route'))
-
- + @auth +

+ {{ __('checkout.page.logged_in_as') }} {{ auth()->user()->email }} +

+ @else + @if ($loginRoute) + + @endif - + + @endauth -
- - - {{-- Abandoned-cart-recovery opt-in. Optional, unticked, never - required — direct marketing under ePrivacy (GR L. 3471/2006 - art. 11), so it needs an explicit opt-in and checkout can't be - gated on it. Narrow scope by design (boboko-core's - setRecoveryConsent) — a general newsletter opt-in, if wanted, - is a separate checkbox. --}} - -
- - {{-- Not wired yet — Modules\Core\Auth\Services\UserOtpService exists - (email + one-time code, passwordless) but nothing in the storefront - calls it yet. UI placeholder only; see project notes. --}} - -
+ {{-- Abandoned-cart-recovery opt-in. Optional, unticked, never + required — direct marketing under ePrivacy (GR L. 3471/2006 + art. 11), so it needs an explicit opt-in and checkout can't be + gated on it. Narrow scope by design (boboko-core's + setRecoveryConsent) — a general newsletter opt-in, if wanted, + is a separate checkbox. --}} +
{{-- Autosaves — no submit button. Any `change` inside .bbk-checkout-main @@ -128,9 +106,25 @@ class="bbk-checkout-tab" -
- - + {{-- Company/ΑΦΜ only when an invoice is wanted. Revealed by CSS + (:has on the checkbox), saved/cleared by saveAddress(), and + required at place-order. --}} +
+ + +
+ + +
@@ -173,8 +167,6 @@ class="bbk-checkout-tab"
- -