From aacc1b0ce7f1f50da04a0a52ba5e94d0695e3ddc Mon Sep 17 00:00:00 2001
From: Maciej <7597086+mdanilowicz@users.noreply.github.com>
Date: Wed, 2 Sep 2026 13:51:11 +0200
Subject: [PATCH 1/3] feat(docs): add registration page and update sidebar and
account documentation
---
apps/docs/.vitepress/sidebar.ts | 4 +
.../src/frontends-recipes/account/index.md | 2 +
.../src/frontends-recipes/account/register.md | 493 ++++++++++++++++++
.../src/frontends-recipes/account/wishlist.md | 2 +-
4 files changed, 500 insertions(+), 1 deletion(-)
create mode 100644 apps/docs/src/frontends-recipes/account/register.md
diff --git a/apps/docs/.vitepress/sidebar.ts b/apps/docs/.vitepress/sidebar.ts
index bba0dd935..aab7806bf 100644
--- a/apps/docs/.vitepress/sidebar.ts
+++ b/apps/docs/.vitepress/sidebar.ts
@@ -241,6 +241,10 @@ export const sidebar = [
text: "Login",
link: "/frontends-recipes/account/login.html",
},
+ {
+ text: "Register",
+ link: "/frontends-recipes/account/register.html",
+ },
{
text: "Wishlist",
link: "/frontends-recipes/account/wishlist.html",
diff --git a/apps/docs/src/frontends-recipes/account/index.md b/apps/docs/src/frontends-recipes/account/index.md
index 1f925fd46..c8b1eeb02 100644
--- a/apps/docs/src/frontends-recipes/account/index.md
+++ b/apps/docs/src/frontends-recipes/account/index.md
@@ -10,4 +10,6 @@ Recipes for customer session and account flows.
+ Check your inbox and open the confirmation link to activate the account. +
+ ++ Signed in as {{ user?.firstName || user?.email }} +
+ + + +``` + +## State And Session + +The Store API identifies the sales channel session with the `sw-context-token` header, and registration runs inside the session the visitor already has. The customer is created against that token, so the guest context the visitor browsed with becomes the customer context without a new token being requested. + +`useUser().register()` assigns the returned customer to the shared customer context only when `active` is true and `doubleOptInRegistration` is false. It then awaits `refreshSessionContext()`, which invokes `readContext get /context` and replaces the reactive session context. `useUser` keeps its customer ref synced with `userFromContext`, so that context response is what `user`, `isLoggedIn`, `isCustomerSession`, and `isGuestSession` are computed from. + +`register()` does not call `refreshCart()`, and `login()` does. If the customer registers while a cart is on screen, call `refreshCart()` from `useCart` yourself, because line item prices, promotions, and rule matches are evaluated against the customer in the context. + +`storefrontUrl` is resolved inside `register()` by `getStorefrontUrl()`, which returns the configured `devStorefrontUrl` or `window.location.origin`. The Store API only accepts a value that matches a configured domain of the sales channel, and it is the base for the confirmation link in the double opt-in email. + +## Edge Cases + +- The generated `billingAddress` type is `Schemas["CustomerAddress"]`, which requires `id` and `customerId`. Send placeholder values as both starter templates do, because you cannot know the ids of an address that does not exist yet. +- A registration with double opt-in enabled resolves successfully while leaving `isLoggedIn` false. Branch on the returned `doubleOptInRegistration` flag instead of assuming a session exists after the promise resolves. +- `refreshSessionContext()` rethrows after logging, so a failing `readContext get /context` rejects the `register()` promise even though the customer was already created. A retry then hits `VIOLATION::CUSTOMER_EMAIL_NOT_UNIQUE`. +- `getStorefrontUrl()` falls back to `window.location.origin`, which is empty during server-side rendering. Submit the form from the client, and set `devStorefrontUrl` when the Shopware domain differs from the origin your app runs on. An origin that is not a configured sales channel domain comes back as a constraint violation pointing at `/storefrontUrl`, which no field in your form owns. +- `guest: true` creates a guest customer that can reuse an email address and needs no password. `isLoggedIn` stays false for that customer because it is computed as `!!id && active && !guest`, while `isGuestSession` becomes true. +- A `requestedGroupId` does not move the customer into that group. It stores the request, and the group has to be available for registration in the current sales channel, which `getCustomerGroupRegistrationInfo get /customer-group-registration/config/{customerGroupId}` reports through `registrationActive` and `registrationOnlyCompanyRegistration`. +- `registerConfirm post /account/register-confirm` needs both `hash` and `em` from the email link, and answers a second click with `CHECKOUT__CUSTOMER_IS_ALREADY_CONFIRMED`. Treat that code as an expected state, not a failure. +- No composable wraps the confirm call, so nothing refreshes the session for you. Call `refreshSessionContext()` after it, otherwise the customer is authenticated in the Store API while your UI still renders as signed out. +- Country states are only present because `useCountries` requests the `states` association. `getStatesForCountry` returns `null` for a country that has none, and a `countryStateId` left over from a previously selected country stays in the form until you clear it. + +## Common Mistakes + +- Do not send a separate address request after registering. The billing address is part of the register body, and `shippingAddress` is a second field in that same body. +- Do not treat a resolved `register()` call as a logged-in customer. Check `doubleOptInRegistration` and `isLoggedIn`. +- Do not set `storefrontUrl` yourself when calling `useUser().register()`. The composable fills it and the parameter type omits it. +- Do not keep a local copy of the registered customer. Read `user`, `isLoggedIn`, and `isGuestSession` from `useUser`. +- Do not leave the cart untouched when registration signs the customer in and prices are already on screen. +- Do not render `error.details.errors` as they arrive. Map `code` to your own copy and use `source.pointer` to place the message next to its field. + +## Testing Checklist + +- Submitting the form calls `register post /account/register` with a `billingAddress` and a `storefrontUrl` the form never set. +- A successful registration without double opt-in refreshes the session context and flips `isLoggedIn` to true. +- A registration with double opt-in renders the confirmation notice and leaves `isLoggedIn` false. +- Registering with an email that already exists shows a mapped message for `VIOLATION::CUSTOMER_EMAIL_NOT_UNIQUE` and keeps the entered values. +- A constraint violation on `billingAddress.zipcode` is rendered next to the postal code field, resolved from `source.pointer`. +- Switching `accountType` to `business` sends `company` and a non-empty `vatIds`. +- Selecting a country with states renders the state select, and selecting one without it does not. +- Opening the confirmation link calls `registerConfirm post /account/register-confirm` with `hash` and `em`, then refreshes the session context. +- A second visit to the confirmation link renders the already-confirmed state instead of an error toast. + +## Related Links + +- [Login recipe](login.html) +- [Composables reference](../../packages/composables/) +- [API client package](../../packages/api-client.html) +- [Cart documentation](../../getting-started/e-commerce/cart.html) diff --git a/apps/docs/src/frontends-recipes/account/wishlist.md b/apps/docs/src/frontends-recipes/account/wishlist.md index f26b5bd82..fc950bd6c 100644 --- a/apps/docs/src/frontends-recipes/account/wishlist.md +++ b/apps/docs/src/frontends-recipes/account/wishlist.md @@ -1,6 +1,6 @@ --- nav: - position: 20 + position: 30 recipe: area: account status: stable From d34a48b8a21e094a35ffcd99e970c901850f3560 Mon Sep 17 00:00:00 2001 From: Maciej <7597086+mdanilowicz@users.noreply.github.com> Date: Mon, 21 Sep 2026 18:21:39 +0200 Subject: [PATCH 2/3] feat(docs): update account recipes with new positions and cart integration --- .../frontends-recipes/account/addresses.md | 2 +- .../frontends-recipes/account/newsletter.md | 2 +- .../src/frontends-recipes/account/register.md | 480 ++++++++++++------ 3 files changed, 317 insertions(+), 167 deletions(-) diff --git a/apps/docs/src/frontends-recipes/account/addresses.md b/apps/docs/src/frontends-recipes/account/addresses.md index a583898d3..8c3c669d2 100644 --- a/apps/docs/src/frontends-recipes/account/addresses.md +++ b/apps/docs/src/frontends-recipes/account/addresses.md @@ -1,6 +1,6 @@ --- nav: - position: 40 + position: 50 recipe: area: account status: stable diff --git a/apps/docs/src/frontends-recipes/account/newsletter.md b/apps/docs/src/frontends-recipes/account/newsletter.md index 2bb67c1ae..5d8354a73 100644 --- a/apps/docs/src/frontends-recipes/account/newsletter.md +++ b/apps/docs/src/frontends-recipes/account/newsletter.md @@ -1,6 +1,6 @@ --- nav: - position: 30 + position: 40 recipe: area: account status: stable diff --git a/apps/docs/src/frontends-recipes/account/register.md b/apps/docs/src/frontends-recipes/account/register.md index 5dfa75b6f..6839d8411 100644 --- a/apps/docs/src/frontends-recipes/account/register.md +++ b/apps/docs/src/frontends-recipes/account/register.md @@ -11,6 +11,7 @@ recipe: - useSalutations - useCountries - useSessionContext + - useCart - useInternationalization - useShopwareContext helpers: @@ -19,6 +20,7 @@ recipe: - register post /account/register - registerConfirm post /account/register-confirm - readContext get /context + - readCart get /checkout/cart - readSalutation post /salutation - readCountry post /country - getCustomerGroupRegistrationInfo get /customer-group-registration/config/{customerGroupId} @@ -34,6 +36,7 @@ recipe: -+
{{ registerError }}
+ +Check your inbox and open the confirmation link to activate the account.
-+
Signed in as {{ user?.firstName || user?.email }}
- + ++ {{ errorsByPointer["/password"] }} +
+ + + + + + + + ``` @@ -448,16 +591,20 @@ The Store API identifies the sales channel session with the `sw-context-token` h `useUser().register()` assigns the returned customer to the shared customer context only when `active` is true and `doubleOptInRegistration` is false. It then awaits `refreshSessionContext()`, which invokes `readContext get /context` and replaces the reactive session context. `useUser` keeps its customer ref synced with `userFromContext`, so that context response is what `user`, `isLoggedIn`, `isCustomerSession`, and `isGuestSession` are computed from. -`register()` does not call `refreshCart()`, and `login()` does. If the customer registers while a cart is on screen, call `refreshCart()` from `useCart` yourself, because line item prices, promotions, and rule matches are evaluated against the customer in the context. +`register()` awaits `refreshCart()` after the context refresh, so line item prices, promotions, and rule matches are re-evaluated against the customer in the context before the promise resolves. `login()` and `logout()` call `refreshCart()` without awaiting it, which is why only `register()` guarantees a settled cart by the time it returns. `storefrontUrl` is resolved inside `register()` by `getStorefrontUrl()`, which returns the configured `devStorefrontUrl` or `window.location.origin`. The Store API only accepts a value that matches a configured domain of the sales channel, and it is the base for the confirmation link in the double opt-in email. ## Edge Cases -- The generated `billingAddress` type is `Schemas["CustomerAddress"]`, which requires `id` and `customerId`. Send placeholder values as both starter templates do, because you cannot know the ids of an address that does not exist yet. +- The generated `billingAddress` type is `Schemas["CustomerAddress"]`, which requires `id` and `customerId`. Send placeholder values as `vue-starter-template` does, because you cannot know the ids of an address that does not exist yet. +- A business account is validated against `billingAddress.company`, not the top-level `company` the generated union declares. Bind your Company input to `form.billingAddress.company` as `vue-starter-template` does; a body that only sets the top-level field comes back with `VIOLATION::IS_BLANK_ERROR` pointing at `/billingAddress/company`. Send the top-level `company` and `vatIds` too, because the union requires them on the business branch. - A registration with double opt-in enabled resolves successfully while leaving `isLoggedIn` false. Branch on the returned `doubleOptInRegistration` flag instead of assuming a session exists after the promise resolves. -- `refreshSessionContext()` rethrows after logging, so a failing `readContext get /context` rejects the `register()` promise even though the customer was already created. A retry then hits `VIOLATION::CUSTOMER_EMAIL_NOT_UNIQUE`. -- `getStorefrontUrl()` falls back to `window.location.origin`, which is empty during server-side rendering. Submit the form from the client, and set `devStorefrontUrl` when the Shopware domain differs from the origin your app runs on. An origin that is not a configured sales channel domain comes back as a constraint violation pointing at `/storefrontUrl`, which no field in your form owns. +- `refreshSessionContext()` rethrows after logging, and the `refreshCart()` that `register()` awaits right after it has no error handling at all, so a failure on either read rejects the `register()` promise even though the customer was already created. A retry then hits `VIOLATION::CUSTOMER_EMAIL_NOT_UNIQUE`, so send the customer to sign-in or password reset instead of inviting another submit. +- `useUser().register()` writes the customer into the shared context before it awaits those two reads, so `isLoggedIn` can already be `true` when the promise rejects. Render your form-level error outside the branch that the form itself lives in, or the message lands in a subtree that has just unmounted. +- A timeout or a dropped connection is not an `ApiClientError`, so it falls through the `instanceof` branch. `apiClientConfig.timeout` has no default, which means an unanswered request leaves a submit button pending indefinitely unless you configure one and branch on `isTimeoutError`. +- The session branches of the form are decided by the customer in the session context, which is anonymous during server-side rendering unless `useUserContextInSSR` is enabled. Put a registration page on a route that opts out of SSR, the way `vue-starter-template` marks `/account/**` with `ssr: false`, or the signed-in branch will hydrate over a server-rendered form. +- `getStorefrontUrl()` falls back to `window.location.origin` whenever `devStorefrontUrl` is unset, and `window` does not exist during server-side rendering, so calling it on the server throws a `ReferenceError` rather than returning an empty string. Submit the form from the client, and set `devStorefrontUrl` when the Shopware domain differs from the origin your app runs on. An origin that is not a configured sales channel domain comes back as a constraint violation pointing at `/storefrontUrl`, which no field in your form owns. - `guest: true` creates a guest customer that can reuse an email address and needs no password. `isLoggedIn` stays false for that customer because it is computed as `!!id && active && !guest`, while `isGuestSession` becomes true. - A `requestedGroupId` does not move the customer into that group. It stores the request, and the group has to be available for registration in the current sales channel, which `getCustomerGroupRegistrationInfo get /customer-group-registration/config/{customerGroupId}` reports through `registrationActive` and `registrationOnlyCompanyRegistration`. - `registerConfirm post /account/register-confirm` needs both `hash` and `em` from the email link, and answers a second click with `CHECKOUT__CUSTOMER_IS_ALREADY_CONFIRMED`. Treat that code as an expected state, not a failure. @@ -470,17 +617,18 @@ The Store API identifies the sales channel session with the `sw-context-token` h - Do not treat a resolved `register()` call as a logged-in customer. Check `doubleOptInRegistration` and `isLoggedIn`. - Do not set `storefrontUrl` yourself when calling `useUser().register()`. The composable fills it and the parameter type omits it. - Do not keep a local copy of the registered customer. Read `user`, `isLoggedIn`, and `isGuestSession` from `useUser`. -- Do not leave the cart untouched when registration signs the customer in and prices are already on screen. +- Do not call `refreshCart()` yourself after `register()`. The composable already awaits one, so a second call is a redundant request. - Do not render `error.details.errors` as they arrive. Map `code` to your own copy and use `source.pointer` to place the message next to its field. ## Testing Checklist - Submitting the form calls `register post /account/register` with a `billingAddress` and a `storefrontUrl` the form never set. -- A successful registration without double opt-in refreshes the session context and flips `isLoggedIn` to true. +- A successful registration without double opt-in refreshes the session context and the cart, and flips `isLoggedIn` to true. - A registration with double opt-in renders the confirmation notice and leaves `isLoggedIn` false. - Registering with an email that already exists shows a mapped message for `VIOLATION::CUSTOMER_EMAIL_NOT_UNIQUE` and keeps the entered values. - A constraint violation on `billingAddress.zipcode` is rendered next to the postal code field, resolved from `source.pointer`. -- Switching `accountType` to `business` sends `company` and a non-empty `vatIds`. +- Switching `accountType` to `business` sends `billingAddress.company`, and the registration is accepted rather than rejected at `/billingAddress/company`. +- A constraint violation on a field the form does not render, such as `/storefrontUrl`, still reaches the customer as a form-level message instead of being dropped. - Selecting a country with states renders the state select, and selecting one without it does not. - Opening the confirmation link calls `registerConfirm post /account/register-confirm` with `hash` and `em`, then refreshes the session context. - A second visit to the confirmation link renders the already-confirmed state instead of an error toast. @@ -488,6 +636,8 @@ The Store API identifies the sales channel session with the `sw-context-token` h ## Related Links - [Login recipe](login.html) +- [Storefront URL](../../guides/storefront-url.html) - [Composables reference](../../packages/composables/) - [API client package](../../packages/api-client.html) - [Cart documentation](../../getting-started/e-commerce/cart.html) +- [devStorefrontUrl troubleshooting](../../resources/troubleshooting.html#what-is-devstorefronturl-and-when-to-use-it) From 8383e3987fccde9a40bd48e797d3d3daf3d839f5 Mon Sep 17 00:00:00 2001 From: Maciej <7597086+mdanilowicz@users.noreply.github.com> Date: Mon, 21 Sep 2026 18:29:17 +0200 Subject: [PATCH 3/3] feat(docs): enhance account registration documentation with new GET routes for salutation and country --- .../src/frontends-recipes/account/register.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/docs/src/frontends-recipes/account/register.md b/apps/docs/src/frontends-recipes/account/register.md index 6839d8411..f62214070 100644 --- a/apps/docs/src/frontends-recipes/account/register.md +++ b/apps/docs/src/frontends-recipes/account/register.md @@ -22,7 +22,9 @@ recipe: - readContext get /context - readCart get /checkout/cart - readSalutation post /salutation + - readSalutationGet get /salutation - readCountry post /country + - readCountryGet get /country - getCustomerGroupRegistrationInfo get /customer-group-registration/config/{customerGroupId} schemas: - Customer @@ -138,8 +140,8 @@ You do not call `readContext get /context` yourself after registering, because ` | Step | Code | Store API | Type | | -------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -| Load salutation options | `fetchSalutations()` | `POST /salutation` |