diff --git a/apps/docs/.vitepress/sidebar.ts b/apps/docs/.vitepress/sidebar.ts
index f937df8a4..069a27cb1 100644
--- a/apps/docs/.vitepress/sidebar.ts
+++ b/apps/docs/.vitepress/sidebar.ts
@@ -242,6 +242,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/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/index.md b/apps/docs/src/frontends-recipes/account/index.md
index e42969c1c..7962a2670 100644
--- a/apps/docs/src/frontends-recipes/account/index.md
+++ b/apps/docs/src/frontends-recipes/account/index.md
@@ -10,6 +10,8 @@ Recipes for customer session and account flows.
{{ registerError }}
+ ++ Check your inbox and open the confirmation link to activate the account. +
+ ++ Signed in as {{ user?.firstName || user?.email }} +
+ + ++ Continuing as a guest with {{ user?.email }}. Create an account below to + keep your order history. +
+ + + + +``` + +## 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()` 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 marks `id` and `customerId` as required. The Store API does not need them — a register body that omits both is accepted — so the empty strings `vue-starter-template` sends are there to satisfy the generated type, not the route. Omit them if your form state is not typed against `CustomerAddress`. +- 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, 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`. +- `useSalutations` and `useCountries` load their lists in `onMounted`, and they keep them in a provided ref rather than in Nuxt state, so nothing is serialized into the payload. Both selects therefore render with only their placeholder option during server-side rendering and fill one tick after hydration. Call `fetchSalutations()` and `fetchCountries()` yourself if you need the options present in the server-rendered HTML. +- 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. +- 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 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 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 `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. + +## 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) diff --git a/apps/docs/src/frontends-recipes/account/wishlist.md b/apps/docs/src/frontends-recipes/account/wishlist.md index 8d036fb8a..afb88519c 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