Skip to content

Customer Account

Signed-in shoppers have an account area at /p/account: a summary of what they have with the shop, and a tab for each kind of record. Every page in it needs a signed-in user (/p/account/** requires ROLE_USER), and everything on it is read for that user. Nothing is looked up by an id from the request, so a shopper can only ever see their own account.

The pages

Page Tab What it shows Extension
/p/account Overview The summary below accountOverview
/p/account/orders Orders Placed orders, newest first, paged; each links to its order page (/o/{id}) orderHistory
/p/account/addresses Addresses The shopper's saved addresses, with the form to add or change one accountAddresses
/p/account/donations Donations Donations by tax year with each year's Gift Aid statement, and a Gift Aid card: the declaration made, or what one would add with the form to make it (in place, no pop-up) userDonationList
/p/account/fundraisers Fundraisers The shopper's fundraisers, and the form to start one userFundraiserList
/p/account/subscriptions Subscriptions Subscriptions, each linking to its page to pause, change or cancel it subscriptionList
/p/account/bookings Bookings Table bookings made while signed in: coming up, then past visits, each linking to its own page to change or cancel accountBookings
/p/account/security Sign-in & security How the shopper signs in, and the browsers they're kept signed in on accountSecurity
/p/account/tickets Tickets The tickets on the shopper's recent orders, as stubs with the QR code, door code and wallet buttons accountTickets
/p/account/preferences Preferences Which mailing lists they get, stop all marketing emails, and the terms of sale they accepted (see Communication preferences) communicationPreferences

The pages are shared by every site (catalogueId "ALL") and use the single-column page layout. Above each one, in topBanner, userAccountMenu draws the header band: the shopper's picture (their Gravatar, else their initials), "Hello, {first name}", their email, a Sign out button, a Site Administration link for staff, and the tabs. A tab shows only when the site has the feature on (donations-enabled, fundraisers-enabled, subscriptions-enabled); Overview, Orders, Addresses and Sign-in & security always show. Tickets shows once the shopper has bought tickets (OrderService.hasPlacedOrderWith, product type eventTicket). Bookings shows when the shop takes table bookings or the shopper has some (see Table bookings). Preferences shows to a customer signed in with their password, not to one recognised only by a remembered cookie.

The overview

accountOverview puts together, for the signed-in shopper:

  • On its way: up to two orders still on their way, first on the page. Each shows up to three of its product pictures, where it is in the order rows' words ("Being packed", "Ready to collect", "Sent"), the store for click & collect, the total, and its steps: Received, Being packed, Sent, Delivered; or Received, Being packed, Ready to collect, Collected. A sent parcel stops counting as on its way 14 days after the order was placed, since few shops mark orders delivered. A collection the store never marked as collected counts as collected once the order is complete (a restaurant's kitchen finishes takeaway orders from its own screen). Absent when nothing is on its way.
  • Tiles: amount donated and the Gift Aid it added; fundraisers live and started; active subscriptions and the next payment date. Each tile links to its tab and shows only when the site has the feature on. There is no orders or spend tile: what a customer has spent isn't something to show on a screen others may see.
  • Recent orders: the last three, with status and total.
  • Gift Aid: whether the shopper has a declaration, when donations are on.
  • Your details: name, email, phone numbers and when they registered, with an Edit your details button that opens the form in place (see Your details). Changing a password goes through the forgot-password flow (no link on sites that use central sign-in, which have no password here).
  • Emails and terms: which of the shop's email lists they get (or that one is waiting for its confirmation link), and the terms of sale version they last accepted, with a link to the Preferences tab, where they change them. It reads the same model as that tab, so the two always agree, and shows only where the tab does. See Communication preferences.
  • Terms changed: one line, We've updated our terms of sale, with a link to the new terms, when the terms text they accepted has changed since. It shows for 90 days from when checkout first showed the new text, and has no dismiss button.
  • Delivery address: their default saved delivery address, with a link to the Addresses tab. A shopper with no saved address sees the delivery address of their newest order that has one (else that order's billing address), dated.

The figures come from the same service methods as the admin customer page (OrderService.summariseForCustomer, DonationService.summariseForCustomer), so the two agree.

Your details

The details card on the overview lets a shopper see and correct the contact details they typed at checkout: their name, contact email and phone numbers. Edit your details opens the form inside the card, and Save details returns to the overview with a confirmation. Only the signed-in owner of the account can change it; a visitor the shop merely recognises is asked to sign in first.

What the form asks for follows the shop's checkout. The fields and the rules come from the personal details step of the shop's checkout page, so shoppers are asked for the same things in both places:

  • Title appears only if the checkout asks for one (the step's Show title setting, or title among its required fields). When the checkout doesn't ask, the title already on the account is left alone.
  • Required fields are the step's Mandatory Fields (by default first and last name). A required field left empty is refused and the form opens again; browsers also stop the empty submit.
  • Email must look like an email address whenever one is given.
  • Phone numbers: the first is the main number; the form lists any others already on file, with one empty box to add another. A number that is already on file keeps its label.

Which email is which. Three addresses can belong to one shopper, and this card changes only one:

  • The contact email is the one the shop writes to about orders. This is what the card edits.
  • The sign-in address is not changed here. The card says so and links to the sign-in settings: the Sign-in tab, or the Badger Auth account page on shops with central sign-in.
  • Marketing emails go to the shop's customer messaging contact, which is a separate record. Changing the contact email here does not change where marketing emails go; shoppers manage those on the communication preferences tab.

The date of birth, if the shop holds one, is not shown and is never changed by this form. The save is a targeted update of just these fields (UserService.updateContactDetails, the call the admin Contact details card uses), so it can't undo a concurrent change to anything else on the account. The rules and the save live in AccountDetailsService; the form posts to POST /account/details (AccountDetailsController). Both themes have the card (Nova and Bootstrap).

The order page

For an hour after payment the order page (/o/{id}) is the confirmation ("Thank you, Sam!"). After that it opens with the order's own head: its number and date, where it is with its steps, and what the shopper can do with it:

  • Buy again (POST /account/orders/{id}/buy-again) puts the order's products back in the basket, each through the normal add-to-basket checks so stock limits apply, and opens it. Tickets, donations, gift cards and subscriptions are left out; the basket URL carries ?boughtAgain=n&skipped=n.
  • Write a review, when product reviews are on and something on the order is still unreviewed.
  • Print the order.

A delivered order names how it comes (delivery on the model, OrderSummaryExtension): the option ("Next-day delivery") beside its expected date or chosen slot. When the shop delivers it itself, by store vans (DELIVER) or local drivers (LOCAL_DELIVERY), the page also says which store it leaves from ("From the Leeds trade counter") and what happens on the day: the option's description ("The driver rings 30 minutes out"), or a general line. That replaces the shop's own "we'll email you when it ships" line (brock.confirm.next), which is for posted orders. The confirmation email adds the same description under the slot.

On a shop that lets customers cancel (customer-cancellation-enabled), the order page also offers Cancel order until the shop starts on the order; see Customers cancelling their own order.

Buy again and reviews are offered only to the signed-in shopper whose order it is, never to someone opening the page with its confirmation code. An in-store sale reads as a receipt ("Bought at York trade counter on 23 September 2026"): its items, the card that paid and the VAT, without the delivery and billing address cards (inStore on the model, for POS_ORDER and POS_QUICK_PAY sales). A walk-in sale with no customer details still opens: the page thanks them without a name and leaves the details card out. The confirmation email of a till sale with an email on file links to this page, as every order email does. Restaurant orders keep their live tracking page instead. Nova and Brock both use this head (account/accountParts :: orderHead).

Tracking a guest order in an account

On a central sign-in shop, a shopper who isn't signed in and opens the order page with its link (from the confirmation, or View order in the order email) sees Track this order in your account near the top, in Nova, Brock and Bootstrap. The shop already has their details, so it's one button: Email me a sign-in code. The panel shows the order's address with most of the name hidden ("j•••@example.com"), since the link may have been forwarded.

The button (POST /o/{id}/{code}/sign-in, OrderSignInController) starts the shop's usual central sign-in. The code only ever goes to the order's own address, and pressing it needs the order's confirmation code. The auth server emails a six-digit code straight away and asks for it. Typing it brings the shopper back to the order page, signed in. The same email has a link for signing in on another device. They don't need a password, and signing in doesn't sign them up for marketing: whatever they chose at checkout stands.

If the browser is already signed in to the auth server as someone else (a shared computer, or another shop with a different address), that sign-in is ended first and the code goes to the order's address, so pressing the button never hands the order page someone else's account (EmailCodeHintFilter). Already signed in with the order's own address, the shopper comes straight back signed in.

The order page link itself stays as it is. It works for weeks, and opens the order on any device without signing in, along with the tickets and kitchen tracking that hang off it. A sign-in code lasts 15 minutes and works once, so it can't replace that link. And opening a link from an email that may have been forwarded must never sign anyone in.

Password guessing

The shop's own sign-in (POST /login, which admins use too) backs off wrong passwords (SignInRateLimiter). Each one counts against the account (the email typed, whether or not anyone has it) and the address it came from. From the 5th on an account (badger.sign-in.failures-per-account), or the 20th from an address across any accounts (badger.sign-in.failures-per-address), the next try has to wait: 30 seconds, then twice as long after each further failure, up to 15 minutes. A failure is forgotten an hour after the last one, and the right password clears the account's count. While waiting, SignInThrottleFilter turns the sign-in away before the password or reCAPTCHA is checked, and the page says "Too many sign-in attempts" (/login?error&throttled, or ?loginError&throttled on the checkout's embedded sign-in). The 15-minute cap keeps it from being a way to lock a customer out for longer. A wrong current password when changing a password counts the same way.

The forgot-password form (PasswordResetRateLimiter) takes 5 requests an hour from an address (badger.password-reset.requests-per-address), then asks it to wait. An email address gets at most 3 reset emails an hour (badger.password-reset.emails-per-email): past that the page still says the email is on its way, the same answer as for an unknown address, but nothing is sent, so nobody can flood a customer's inbox or use the form to find accounts.

Both count in Redis per shop, hold hashes rather than addresses or emails (neither is logged), and fail open if Redis is down. Sites on central sign-in use the auth server's own lockout instead.

On a site with reCAPTCHA (its recaptchaSiteKey and recaptchaSecretKey settings, under Security and access), RecaptchaAuthenticationFilter checks every POST /login and sends a failed check to /login?recaptcha=failure, where the page says it couldn't check the shopper isn't a robot.

Themes

The account area is styled in Nova (.nova-account-* in bdgr.css), so every theme layered on Nova gets it. A theme recolours it through custom properties on .nova-account: --account-band-bg, --account-band-ink, --account-band-muted, --account-page-bg (the page background, which the current tab joins), --account-figure, --account-figure-strong and --account-display-font. Ridge sets them in ridge.css (purple band, red figures, Barlow Condensed).

The legacy Bootstrap theme keeps its own account templates.

Sign-in & security

The Sign-in & security tab (accountSecurity, actions in AccountSecurityController) shows the email the shopper signs in with, then:

  • On a shop with its own sign-in: a form to change their password (current password, then the new one twice, at least 8 characters). A wrong current password counts as a wrong sign-in for that account (SignInRateLimiter), so after a few the form asks them to wait. The overview's "Change your password" link comes here. A new password ends every "Remember me" login, this browser's too; sessions carry on.
  • On a central sign-in shop: the Badger Auth widget (below) instead of the form, since passwords and passkeys are kept on the auth server.
  • Where you're signed in: the browsers that keep them signed in ("Remember me"), as "Chrome on macOS" with when each was last used, and Sign out everywhere. After a confirming second step it ends every "Remember me" login and every session, this one too, and goes back to the home page signed out.

Signing in by email

On a central sign-in shop, signing in by email is the default for every customer. They enter their address, press Email me a sign-in code, and type the six-digit code from the email, or open its link on any device. An address with no account gets one when the code or link is first used, never just for asking. Passwords and passkeys are still there for anyone who wants them (Sign in with a password, or set one up from the account page).

  • One email, two ways in. The code is typed on the page that asked for it, so the shopper stays in the same browser tab. The link opens a page with a Continue button, and only that button uses it, so mail scanners that open every link don't spend it. Whichever is used first, the other stops working.
  • Limits. A code and link last 15 minutes. Five wrong codes and that code is spent. An address gets 5 emails, then has to wait 15 minutes for the next, twice as long after each one after that, up to 4 hours (badger.auth.email-sign-in.sends-before-wait). The count is forgotten a day after the last email, or when the address signs in by email. An IP address can ask for at most 30 emails an hour. The "check your email" page looks the same whether or not an email went, so it doesn't reveal who has an account.
  • Guest orders join the account. A code or link proves the shopper reads that address, so the ID token says email_verified and the shop gives the account the orders placed on it as a guest with that address (OrderService.claimGuestOrders). Orders already on another registered account are left alone. Passwords, passkeys and Google or Apple sign-in don't claim anything: anyone can type an address at checkout, and those sign-ins don't show they read its mail.
  • Badger Sett operators who must sign in with Sett are refused here too, as with passwords and passkeys.

How it's built: EmailSignInService (challenges in auth_email_sign_in, hashed and removed by Mongo when they expire), EmailSignInFlow and EmailSignInController on the auth server; the shop's encrypted login_hint carries intent: email-code and the order page path when the shopper pressed the order page's button.

Switching central sign-in on

A superadmin switches it on from System > Tenants > {site} > Enable central sign-in, which registers the shop's OIDC client (oauth2-client-id) and sets central-auth-enabled. A shop needs both: with the flag set but no client, the storefront falls back to its own sign-in page and logs a warning. The tenant page shows that state as Switched on, but not working, with Enable central sign-in to register a client.

The Badger Auth widget

On a central sign-in shop (central-auth-enabled) the tab, and the older userProfile block, show the auth server's profile card in an iframe (ProfileWidgetService, /widget/profile on the auth server):

  • Passkeys (how many, or "Add a passkey"), Password ("Change password", or "Set a password" for someone who only signs in with Google or Apple) and each connected Google or Apple account.
  • The links open the auth portal's account page in the same tab, at the right section (#passkeys, #password), with a link back to the shop's Sign-in & security tab.
  • The links and buttons take the shop's brand colour (email-brand-primary-color).

How it's embedded:

  • The page posts a v2 embed token (EMBED, purpose profile-widget) into the iframe with a hidden form, so the customer's email isn't in the iframe's URL, the auth server's access log or a Referer header.
  • The token is signed with the shop's widget key and names the key, so the auth server checks it with the current key and then the previous one, and a key rotation doesn't break the card. It lasts 15 minutes and binds the page the links come back to; the auth server only honours a return page on one of the shop's own domains.
  • Only the shop may frame the card: the auth server sends frame-ancestors with the shop's origins.
  • The card posts its height to the shop's origin only (and again whenever it changes), and the page only listens to messages from the card's own origin and window. The iframe's sandbox lets its links take the whole tab, but only when the customer clicks.

The old GET /widget/profile?token= (v1 tokens) still answers, for pages cached before the change.

The auth portal's account page

The card's links open the auth portal's account page (/account?returnTo=<shop page>). Opened from a shop (a returnTo on one of the customer's shops), it:

  • wears that shop's logo and says "Your account for {shop}", with Return back to the page it came from;
  • keeps the way back through its own forms (display name, password, disconnecting an account);
  • lets the customer Disconnect Google or Apple, unless it's their only way in: the account must still have a password, a passkey or the other connected account;
  • signs out back to the shop: Sign out sends the shop's site id, and /logout/complete looks its address up in the database (never a URL from the request), as it does for a shop's own sign-out. Opened directly, sign-out lands on the sign-in page as before.

On the storefront's sign-in block (authenticationExtension), a central sign-in shop now shows the Sign in button in full mode too, rather than a password form, in Nova and Bootstrap.

Contact details at checkout

A signed-in shopper's checkout starts with the contact details on their account, rather than those of their last order, as long as the account has a first name, last name and email. A title or phone number the account lacks is taken from the last order, and the form still asks for anything the shop requires that is missing. An account without a name and email falls back to the last order, as before.

When the shopper places their own order, the account is brought up to date with what they entered: name, contact email, phone numbers, and the title if they gave one (UpdateAccountContactDetailsProcessor, in the submitOrder pipeline). A field left blank never erases what the account holds. The email they sign in with is never changed, only the contact email. As with saved addresses, nothing is updated for a visitor the site only recognises by its cookie, for a guest, or when staff place or capture an order (POS, admin). For those, and for details given elsewhere (a contact form, say), the account only has empty fields filled in.

Saved addresses

Each account has an address book (CustomerAddress, the customerAddress collection, through CustomerAddressService), kept per site:

  • Filled in by ordering. When a signed-in shopper places an order, its delivery and billing addresses are saved (SaveOrderAddressesProcessor, in the submitOrder pipeline). An address already in the book (same first line, postcode and country, ignoring case, spaces and punctuation) is marked as just used rather than added again. A shopper's first addresses become their defaults. A collected order's address isn't saved, since it is the shop's.
  • Only when they signed in. A shopper the site only recognises by its cookie gets nothing saved, and nothing is saved when staff place or capture an order (POS, admin), since they are signed in as themselves. The book is shown at checkout under the same rule as the details prefill: only to a shopper signed in on that request.
  • Managed on the Addresses tab. Add, change and remove addresses, give one a name ("Home", "Work"), and choose the default for delivery and for billing (AccountAddressesController). Removing a default hands it to the most recently used address left. The form checks the shop's required address fields (mandatoryAddressFields). A book keeps at most 20 addresses; past that, the least recently used address that isn't a default goes.
  • Corrected by staff. On the customer's page in the admin, the Addresses card lists their book and lets staff add, change, remove and set the defaults for delivery and for billing (UserAdminController, through CustomerAddressService.saveByStaff). The same required-field check applies. A staff add or change is recorded on the address (editedByStaffId, editedByStaffAt) and the card shows "Changed by staff on" with the date; the customer saving that address themself clears it. The shopper's Addresses tab doesn't show it.
  • Offered at checkout. The single-page checkout shows the saved addresses as cards above the delivery fields (CheckoutSessionDTO.savedAddresses). Choosing one fills the fields and puts them away; "Use a different address" opens them, empty. The default saved addresses also prefill a new checkout, in place of the last order's (ExpressCheckoutService).

Saved addresses follow the account: a merge moves them to the account kept, the marked-for-deletion cleaner deletes them with the account (or when it anonymises one), deleting a site deletes its addresses, and they are listed first in a subject access request pack. They are customer data, so a site transfer doesn't export them.

Moving existing sites

The system update 2026-10-21-move-account-pages-to-tabs (MoveAccountPagesToTabs) moves account pages that were still on the old two-column layout (page-3-9): it sets the page template, moves userAccountMenu to topBanner, switches off the left column's sign-in card (authenticationExtension; the band has Sign out) and adds accountOverview to /p/account. A page whose layout someone has since changed is left alone. Page caches expire within 30 minutes, so the new layout can take that long to show.

The system update 2026-10-23-add-missing-account-pages (AddMissingAccountPages) installs any shared account page an install doesn't have, from defaultPages.json, in the tabbed layout: the new Tickets tab, and any of the others an older install lacks. 2026-10-24-add-account-addresses-page runs it again for the Addresses tab, 2026-10-25-add-account-security-page for Sign-in & security and 2026-10-26-add-account-bookings-page for Bookings. defaultPages.json holds all ten pages in the tabbed layout, so a new install starts with them (before, a fresh database got the old two-column overview and no tabs). The Preferences page is added by 2026-10-26-add-account-preferences-page, already on the tabbed layout, and is in defaultPages.json too.

Design notes: Account area.