Application initialization
Application Load · Nuxt
Application Load is the first request your storefront makes to its personal backend. It returns the settings and initial content needed to render the interface, the current customer profile when signed in, and the API methods available to this session.
/api/apploadWhat the response contains
page_data — the application data inside the API response. Its four main sections have different roles:
Initial storefront data, customer profile and available API endpoints.
app/interfaces/orionplace/appBase/IAppLoad.tsimport type { IAppLoadCommon } from '@/interfaces/orionplace/appBase/IAppLoadCommon';
import type { IApploadUserProfile } from '@/interfaces/orionplace/appBase/IApploadUserProfile';
import type { IAppLoadPathsRecord } from '@/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonPath';
export interface IAppLoad {
// Shared storefront settings and initial data.
init: IAppLoadCommon;
// Public API methods, grouped under paths.
unauth: IAppLoadPathsRecord;
// Current customer profile. Omitted for guests.
user?: IApploadUserProfile;
// Authenticated-session methods. Omitted for guests; the backend checks
// permissions.
auth?: IAppLoadPathsRecord;
// Whether seller-panel features are enabled for this storefront.
seller_panel_enabled?: boolean;
// Product-preview key for a signed-in account with seller information.
// Do not publish its value; buyer-facing interfaces do not need this key.
view_key?: string;
// Backend Application Load preparation time in seconds; only with project
// debug enabled.
// Excludes response serialization and network transfer.
spent_total?: number;
// Backend init loading time in seconds. Only with project debug enabled.
spent_init?: number;
// Public API discovery loading time in seconds. Only with project debug
// enabled.
unauth_spent?: number;
// Profile and auth preparation time in seconds. Debug only; zero for guests.
auth_spent?: number;
}
What to build with init
Each field links to its data reference, actual consumers and related component guides.
page_data.init/api/apploadLanguage, currencies and units
Languages, currencies and measurement units, including names, display formats and defaults for each locale.
localestore.localesMainLanguageSwitcherselects the language; the store applies its default currency and units to the current selections.currenciesstore.currenciesCurrency choices read by
MainCurrencySwitcher.weightsstore.weightsWeight-unit choices read by
UnitSwitcher.lengthsstore.lengthsLength-unit choices read by
UnitSwitcher.shipping_countriesstore.init?.shipping_countriesThe shop's shipping countries, named in the product pages' return terms.
Catalog and search
Categories, initial product collections, search suggestions and subscription tariffs.
catalogstore.catalogCategory tree used by
CatalogHeaderandCategoryGroup.productsstore.productsInitial products read by
ProductGroup,ProductCardand product components such asHoverImageViewer.featuredstore.featuredPersonalized recommendations based on previous searches and categories. Product components read them with
collection="featured".sellersstore.init?.sellersSeller details keyed by ID. Each Application Load product identifies its seller with
seller_id; the product store resolves that reference when components read the product.search_hintsstore.init?.search_hintsLocalized suggestions displayed by
MainSearch.tariffsstore.tariffsTariff collection read by
TariffCardsto render individualTariffCardcomponents.
Branding and page metadata
Logos, the site icon, storefront display settings and SEO metadata.
logosstore.init?.logosBrandLogosdisplays the selected logo size or icon.visual_settingsstore.init?.visual_settingsCategory menu images, home category count and ordering, and other storefront display settings.
seostore.init?.seogetStorefrontSeo()prepares metadata forapp/pages/index.vue: the title, description and keywords useuseSeoMeta(), while additional tags useuseHead().
Content and contacts
Banners, storefront events, company contacts and social links.
main_bannerstore.init?.main_bannerBanner images displayed by
MainBannerin the current storefront language.eventsstore.init?.eventsEvent images displayed by
EventsBlock, filtered by language andis_layout.contactstore.init?.contactCompany contacts and registration details displayed by
AboutPage.smstore.init?.smSocial links and localized captions displayed by
SocialLinks.
Customer and counters
Authentication status, wallet balance and initial cart and wishlist counters. The full cart and account data are available through separate API endpoints.
is_authenticatedstore.isAuthenticatedstore.isAuthenticatedcontrols authenticated discovery and switchesStorefrontAccountbetween Login and the profile/wallet menu.walletstore.init?.walletWalletBalancedisplays the selected currency balance, including compact mode inStorefrontAccount.variablesstore.init?.variablesInitial counts for the cart and wishlist buttons, preorder permission and content-status defaults.
Maps and integrations
Map center, public map key and availability of address lookup from coordinates.
default_mapstore.init?.default_mapDefault map center coordinates.
geo_location_enabledstore.init?.geo_location_enabledIndicates whether address lookup by coordinates is available. Text suggestions depend on the selected country.
public_keysstore.init?.public_keysmap_keyloads Yandex Maps for the delivery map in the cart.
Fields depend on store settings and the current session. A field may be absent or a collection empty. Initial products do not replace a complete, paginated catalog.
Use the data after initialization
app/middleware/00.orionplace.global.tsGlobal middleware runs before each page and waits for initialization.
Once data is loaded, navigation does not repeat the request. If loading fails, the store records the failure and the middleware opens the storefront error page with HTTP 503 and a Retry button that reloads the page. An unknown address keeps its 404 page. The middleware itself does not display notifications.
Source code
import { abortNavigation, defineNuxtRouteMiddleware } from '#imports'; import { useOrionplaceAppLoad } from '@/composables/useOrionplaceAppLoad'; import { storefrontPageError } from '@/composables/useOrionplaceMissingPage'; /** * Bootstrap precedes page setup. A failed Application Load opens the * storefront error page with HTTP 503 on every page; an unknown address keeps * its 404 page. */ export default defineNuxtRouteMiddleware(async (to) => { const { initialize, store } = useOrionplaceAppLoad(); const locale = typeof to.query.locale === 'string' ? to.query.locale : ''; await initialize(locale); if (!to.matched.length) { return; } const error = storefrontPageError(store.status, false, ''); if (error) { return abortNavigation(error); } });app/composables/useOrionplaceAppLoad.tsThe composable calls the shared API client, validates the initialization response and passes the result to the store. The client's transport relays the cookies that the backend sets during a server render.
It provides initialize, refresh and reset actions. Accessing it alone does not start a request.
Source code
import { createError, usePinia, useResponseHeader } from '#imports'; import { computed } from 'vue'; import { storeToRefs } from 'pinia'; import { useOrionplaceStore } from '@/stores/orionplace'; import type { AppLoadFailure, AppLoadResult, } from '@/interfaces/orionplace/transport'; import { useOrionplaceApi } from '@/composables/useOrionplaceApi'; import { appLoadRejectedLocale, appLoadResult, inspectAppLoad, } from '@/utils/orionplaceAppLoad'; /** Transport and explicit actions. Calling this accessor does not start a request. */ export function useOrionplaceAppLoad() { // SSR forwards the current request's domain, cookies and authorization to our own gateway. const store = useOrionplaceStore(usePinia()); const refs = storeToRefs(store); const api = useOrionplaceApi(); if (import.meta.server) { useResponseHeader('cache-control').value = 'private, no-store'; } const request = async ( signal: AbortSignal, locale = '', ): Promise<AppLoadResult> => { let received: { body: unknown; status: number; cookies: string[] } | undefined; try { received = await api.applicationLoad(signal, locale); } catch { throw createError({ message: 'Could not reach the marketplace API.', data: { kind: 'network', httpStatus: null, fields: [], } satisfies AppLoadFailure, }); } if (!received) { throw createError({ message: 'The API returned no response.', data: { kind: 'network', httpStatus: null, fields: [], } satisfies AppLoadFailure, }); } // A cookie that AppLoad sets during a server render (a new guest session // or CSRF token) is relayed to the browser by the shared API transport, // which also sends it with the later requests of the same render. const failure = inspectAppLoad(received.body, received.status); // An address with a language the storefront does not have (`?locale=xx`) // opens in the storefront's default language instead of the 503 page. if (failure && locale && appLoadRejectedLocale(failure)) { return request(signal); } if (failure) { throw createError({ message: 'Marketplace initialization failed.', data: failure, }); } return appLoadResult(received.body, received.status); }; return { store, data: refs.result, status: refs.status, error: computed(() => (store.failure ? { data: store.failure } : null)), initialize: (locale = '') => store.load((signal) => request(signal, locale)), refresh: () => store.load(request, true), // Called by the shared API client after HTTP 401 in a signed-in session. endSession: () => store.endSession(request), reset: () => store.reset(), }; }app/composables/useOrionplaceApi.tsCreates the shared API clients of one Nuxt application: one for the browser session and one for each server render. Components send requests to discovered endpoints in the browser. The product, category, search, featured, journal and article pages also load their first data during the server render through
useOrionplaceServerApi(), with the visitor's cookies, so the page HTML already contains that data. The browser reuses the result without repeating the request.Source code
import { useNuxtApp, usePinia, useRequestFetch, useRequestHeader, useResponseHeader, useRuntimeConfig, } from '#imports'; import { useOrionplaceStore } from '@/stores/orionplace'; import { useOrionplaceSelectionsStore } from '@/stores/orionplaceSelections'; import { useOrionplaceAppLoad } from '@/composables/useOrionplaceAppLoad'; import { OrionplaceApi } from '@/utils/orionplace/api'; import type { OrionplaceApiRequest, OrionplaceApiResponse, } from '@/utils/orionplace/api'; import { createOrionplaceCookieJar } from '@/utils/orionplace/serverCookies'; import { nativeAppCookies, nativeAppOrigin, nativeAppRequest, } from '@/utils/orionplace/nativeApp'; interface OrionplaceClients { browser: OrionplaceApi; server: OrionplaceApi; } // One entry per Nuxt application: one browser session, or one server render. const clients = new WeakMap<object, OrionplaceClients>(); function createClients(): OrionplaceClients { const nuxt = useNuxtApp(); const store = useOrionplaceStore(usePinia()); const selections = useOrionplaceSelectionsStore(); const requestFetch = useRequestFetch(); // A server render sends the visitor's cookies and the cookies the backend // set earlier in the same render; see utils/orionplace/serverCookies.ts. const jar = import.meta.server ? createOrionplaceCookieJar(useRequestHeader('cookie')) : undefined; const responseCookies = import.meta.server ? useResponseHeader('set-cookie') : undefined; // The Android app sends its requests to the public storefront; see // utils/orionplace/nativeApp.ts. Empty on the website. const appOrigin = nativeAppOrigin( useRuntimeConfig().public.orionplaceAppOrigin, ); // Every cookie the backend sets during a server render reaches the // browser as its own Set-Cookie header, unchanged. function relay(cookies: string[]) { if (!responseCookies || !cookies.length) { return; } const existing = responseCookies.value; const existingCookies = Array.isArray(existing) ? [...existing] : []; if (existing && !Array.isArray(existing)) { existingCookies.push(String(existing)); } responseCookies.value = [...existingCookies, ...cookies]; } const transport = async ( path: string, request: OrionplaceApiRequest, ): Promise<OrionplaceApiResponse> => { if (appOrigin) { return nativeAppRequest(appOrigin, path, request); } let received: OrionplaceApiResponse | undefined; // A server render's own API call copies the visitor's request headers. // A visitor who follows a link from another site arrives with // `Sec-Fetch-Site: cross-site` (and a form post with a foreign Origin); // the gateway's cross-site check would refuse the render's call and the // page would answer 503. The call comes from this server, so it is sent // as same-origin without the visitor's Origin. const headers = jar ? { ...request.headers, cookie: jar.header(), origin: '', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', } : request.headers; await requestFetch<unknown>(path, { method: request.method, headers, body: request.method === 'GET' ? undefined : request.data, query: request.method === 'GET' && !(request.data instanceof FormData) ? request.data : undefined, signal: request.signal, credentials: 'same-origin', cache: 'no-store', retry: 0, timeout: 35_000, ignoreResponseError: true, onResponse({ response }) { received = { body: response._data, status: response.status, cookies: import.meta.server ? response.headers.getSetCookie() : [], headers: { 'content-type': response.headers.get('content-type') ?? '', 'content-disposition': response.headers.get('content-disposition') ?? '', }, }; }, }); if (!received) { throw new Error('The API returned no response.'); } jar?.apply(received.cookies); relay(received.cookies); return received; }; const shared = { transport, endpoints: () => store.endpoints, cookies: () => jar ? jar.header() : appOrigin ? nativeAppCookies() : document.cookie, locale: () => store.localeCode, isSignedIn: () => store.isAuthenticated, // The customer's currency for orders and plan purchases (CURRENCY_REQUESTS). currency: () => selections.currencyId, }; return { // Components send their requests in the browser. During a server render // this client sends only Application Load, so a request started while // the page renders cannot outlive the render. browser: new OrionplaceApi({ ...shared, canRequest: () => import.meta.client, // HTTP 401 ended the customer's session: show the guest storefront. // This never opens sign-in and never repeats the failed request. onSessionEnded: () => nuxt.runWithContext(() => useOrionplaceAppLoad().endSession()), }), // Page data that the server render awaits; see useOrionplaceServerData. // A failed server request leaves the page to the browser, which loads // the data again and handles an ended session there. server: new OrionplaceApi(shared), }; } function useOrionplaceClients() { const nuxt = useNuxtApp(); const existing = clients.get(nuxt); if (existing) { return existing; } const created = createClients(); clients.set(nuxt, created); return created; } export function useOrionplaceApi() { return useOrionplaceClients().browser; } /** * The client for page data loaded during a server render. It shares the * transport, endpoint discovery, cookies and CSRF handling with * `useOrionplaceApi`. Use it through `useOrionplaceServerData`, which * awaits the request before the page renders. */ export function useOrionplaceServerApi() { return useOrionplaceClients().server; }app/utils/orionplace/api.tsOwns the Application Load bootstrap and resolves browser requests by
url_namefrom current discovery. It reads the current CSRF token per request and deduplicates renewal. A CSRF rejection renews the token for the next action and returns the error. The customer retries explicitly; failed requests are never repeated automatically.Source code
import type { IAppLoadPathObject } from '@/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonPath'; import type { TMethod } from '@/interfaces/orionplace/apiBase/TMethod'; export interface OrionplaceApiResponse { body: unknown; status: number; cookies: string[]; headers?: Record<string, string>; } export type OrionplaceApiData = Record<string, unknown> | FormData; export interface OrionplaceApiRequest { method: TMethod; data: OrionplaceApiData; headers: Record<string, string>; signal?: AbortSignal; } export type OrionplaceTransport = ( path: string, request: OrionplaceApiRequest, ) => Promise<OrionplaceApiResponse>; /** * The texts of the field errors, each once: OrionMarketplace often repeats a * field's error for the whole form. */ export function fieldErrorText(fields: Record<string, unknown>) { return [...new Set(Object.values(fields).filter(Boolean))].join(' '); } export class OrionplaceApiError extends Error { readonly kind: 'unavailable' | 'failed'; readonly fields: Record<string, string>; readonly status?: number; readonly data?: unknown; // Error codes of the answer, such as `sign_in` when OrionMarketplace asks the visitor // to sign in first. readonly codes: string[]; constructor( kind: 'unavailable' | 'failed', fields: Record<string, string> = {}, status?: number, data?: unknown, codes: string[] = [], ) { super(fieldErrorText(fields) || kind); this.kind = kind; this.fields = fields; this.status = status; this.data = data; this.codes = codes; } } /** * Requests whose amounts the backend counts in the customer's currency: an * order, a preorder and a plan purchase are priced and charged in it. The * client adds the selected currency to them (unless the caller set one). */ export const CURRENCY_REQUESTS = new Set([ 'orders_create', 'preorders_create', 'subscriptions_subscribe', 'subscriptions_renew', 'subscriptions_change_renew', 'subscriptions_change_tariff', 'subscriptions_change_option', ]); export class OrionplaceApi { private renewal: Promise<void> | undefined; private sessionEnding: Promise<void> | undefined; private transport: OrionplaceTransport; private endpoints: () => IAppLoadPathObject; private cookies: () => string; private locale: () => string; private canRequest: () => boolean; private isSignedIn: () => boolean; private currency: () => string | number | undefined; private onSessionEnded: (() => unknown) | undefined; constructor(options: { transport: OrionplaceTransport; endpoints: () => IAppLoadPathObject; cookies: () => string; locale?: () => string; canRequest?: () => boolean; isSignedIn?: () => boolean; currency?: () => string | number | undefined; onSessionEnded?: () => unknown; }) { this.transport = options.transport; this.endpoints = options.endpoints; this.cookies = options.cookies; this.locale = options.locale ?? (() => ''); this.canRequest = options.canRequest ?? (() => true); this.isSignedIn = options.isSignedIn ?? (() => false); this.currency = options.currency ?? (() => undefined); this.onSessionEnded = options.onSessionEnded; } /** The guest refresh started by the latest HTTP 401, while it runs. */ get sessionEnd() { return this.sessionEnding; } applicationLoad(signal: AbortSignal, locale = '') { return this.send( '/api/appload', 'POST', { detalize_mode: 'light', ignore_cache: false, }, signal, locale, ); } async request<T>( name: string, data: OrionplaceApiData, signal?: AbortSignal, params: Record<string, number> = {}, ): Promise<T> { return this.successData<T>( await this.requestResponse(name, data, signal, params), ); } async requestResponse( name: string, data: OrionplaceApiData, signal?: AbortSignal, params: Record<string, number> = {}, ): Promise<OrionplaceApiResponse> { if (!this.canRequest()) { throw new OrionplaceApiError('unavailable'); } const discovered = Object.values(this.endpoints()) .flatMap((section) => Object.values(section)) .find((entry) => entry.url_name === name); const endpoint = discovered ?? (name === 'reload_customer_app' ? { target: 'api/reload_customer_app', methods: ['PATCH' as const] } : undefined); const method = endpoint?.methods[0]; let path = `/${endpoint?.target.replace(/^\/+/, '') ?? ''}`; path = path.replace(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g, (_, key: string) => { const value = params[key]; if (!Number.isSafeInteger(value) || Number(value) <= 0) { throw new OrionplaceApiError('unavailable'); } return String(value); }); if ( !endpoint || !method || !/^\/api\/[a-zA-Z0-9_/-]+(?:\.png)?$/.test(path) ) { throw new OrionplaceApiError('unavailable'); } if (method === 'GET' && data instanceof FormData) { throw new OrionplaceApiError('unavailable'); } const currency = Number(this.currency()); if ( CURRENCY_REQUESTS.has(name) && !(data instanceof FormData) && data.currency === undefined && Number.isSafeInteger(currency) && currency > 0 ) { data = { ...data, currency }; } const protectedRequest = method !== 'GET'; if (protectedRequest && !this.csrfToken()) { await this.renewCsrf(); } signal?.throwIfAborted(); const response = await this.send(path, method, data, signal); const body = response.body as { page_data?: { csrf?: boolean }; } | null; // A CSRF rejection: Django views answer 419, API endpoints answer 403 // with page_data.csrf set to false. The request is not repeated. const csrfRejected = response.status === 419 || (response.status === 403 && body?.page_data?.csrf === false); // Renew for the next explicit action and retain this request's rejection. if (protectedRequest && csrfRejected) { await this.renewCsrf(); signal?.throwIfAborted(); } this.detectSessionEnd(response); signal?.throwIfAborted(); return response; } /* * The backend answers HTTP 401 when the request has no signed-in session: * an authentication-required endpoint without a customer ({ auth: false }), * or a session the backend rejected and whose cookies it deleted. * A 401 while the application still shows a customer means the session * ended. One handler call serves concurrent failures; a guest 401, 403 and * CSRF rejection are ordinary request errors. */ private detectSessionEnd(response: OrionplaceApiResponse) { if ( response.status !== 401 || this.sessionEnding || !this.onSessionEnded || !this.isSignedIn() ) { return; } let ending: Promise<void>; try { ending = Promise.resolve(this.onSessionEnded()).then(() => undefined); } catch (error) { ending = Promise.reject(error); } // The failed request reports its own error; the refresh has its own state. this.sessionEnding = ending .catch(() => undefined) .finally(() => { this.sessionEnding = undefined; }); } private csrfToken() { return this.cookies().match(/(?:^|;\s*)csrftoken=([^;]*)/)?.[1] ?? ''; } private localizedPath(path: string, requestedLocale = '') { const locale = (requestedLocale || this.locale()).trim(); if (!locale) { return path; } const separator = path.includes('?') ? '&' : '?'; return `${path}${separator}lang=${encodeURIComponent(locale)}`; } private async renewCsrf() { if (!this.renewal) { this.renewal = this.send('/api/csrf_renew', 'POST', {}) .then((response) => { this.detectSessionEnd(response); this.successData(response); if (!this.csrfToken()) { throw new OrionplaceApiError('failed'); } }) .finally(() => { this.renewal = undefined; }); } await this.renewal; } private send( path: string, method: TMethod, data: OrionplaceApiData, signal?: AbortSignal, locale = '', ) { const headers: Record<string, string> = { Accept: 'application/json', ...(data instanceof FormData ? {} : { 'Content-Type': 'application/json' }), }; const token = this.csrfToken(); if (token) { headers['X-CSRFToken'] = token; } return this.transport(this.localizedPath(path, locale), { method, data, headers, signal, }); } private successData<T>(response: OrionplaceApiResponse): T { const body = response.body as { form_errors?: unknown; success_message?: unknown; page_data?: T; } | null; if ( response.status < 200 || response.status >= 300 || body?.form_errors !== null || typeof body.success_message !== 'string' || !body.success_message ) { const fields = Object.fromEntries( Object.entries(body?.form_errors ?? {}).map(([name, entries]) => [ name, Array.isArray(entries) ? entries.map((entry) => String(entry?.message ?? '')).join(' ') : '', ]), ); const codes = Object.values(body?.form_errors ?? {}) .flatMap((entries) => (Array.isArray(entries) ? entries : [])) .map((entry) => String(entry?.code ?? '')) .filter(Boolean); throw new OrionplaceApiError( 'failed', fields, response.status, body?.page_data, codes, ); } return body.page_data as T; } }app/stores/orionplace.tsuseOrionplaceStore()- Component accessor for the coordinated stores shown below.
store.result- Load result: response body, HTTP status and retrieval time.
store.result?.response- Complete API response body.
store.data- Application data from the API response.
store.init- Initial storefront data. The links above lead to each field, its component and source code.
store.profile- Customer profile, when included in the response.
useOrionplaceStore()owns the Application Load response, load status, profile, wallet and endpoint discovery. Components read selections, catalog data, products and browse state through their domain store hooks. It coordinates refresh, reset and session changes across the smaller stores.Source code
import type { IApploadUserProfile } from '@/interfaces/orionplace/appBase/IApploadUserProfile'; import { computed, ref, shallowRef, hasInjectionContext, inject, provide, } from 'vue'; import type { InjectionKey, Ref } from 'vue'; import type { Pinia } from 'pinia'; import { defineStore, getActivePinia, storeToRefs } from 'pinia'; import { useOrionplaceSelections } from '@/stores/orionplaceSelections'; import { useOrionplaceBrowse } from '@/stores/orionplaceBrowse'; import { useOrionplaceCatalog } from '@/stores/orionplaceCatalog'; import { useOrionplaceProducts } from '@/stores/orionplaceProducts'; import { attachOrionplaceDomainStores, registerOrionplaceDomainResolver, registerOrionplaceDomainStores, releaseOrionplaceDomainStores, } from '@/stores/orionplaceDomainAccess'; import { guestAppLoadResult, guestProduct, } from '@/utils/orionplace/guestSession'; import type { IProduct } from '@/interfaces/orionplace/products/IProduct'; import type { AppLoadFailure, AppLoadResult, AppLoadLoader, AppLoadStatus, IAppLoadPathObject, } from '@/interfaces/orionplace'; function forwardedState<T>(source: Ref<T>) { return computed({ get: () => source.value, set: (value: T) => { source.value = value; }, }); } /** One instance per Nuxt SSR request; @pinia/nuxt hydrates it into the browser. */ function createStoreState(scope: string, pinia: Pinia) { const result = shallowRef<AppLoadResult | null>(null); const status = ref<AppLoadStatus>('idle'); const failure = shallowRef<AppLoadFailure | null>(null); const isReady = computed( () => status.value === 'success' && result.value !== null, ); const data = computed(() => result.value?.response.page_data ?? null); const init = computed(() => data.value?.init ?? null); const profile = computed(() => data.value?.user ?? null); const isAuthenticated = computed(() => init.value?.is_authenticated ?? false); const selections = useOrionplaceSelections( scope, pinia, init, ); const selectionRefs = storeToRefs(selections); const { locales, currencies, weights, lengths, currentLocale, currentCurrency, } = selectionRefs; const localeCode = forwardedState(selectionRefs.localeCode); const currencyId = forwardedState(selectionRefs.currencyId); const weightId = forwardedState(selectionRefs.weightId); const lengthId = forwardedState(selectionRefs.lengthId); const { setLocale, setCurrency, setUnit, } = selections; const catalogState = useOrionplaceCatalog( scope, pinia, init, ); const catalogRefs = storeToRefs(catalogState); const { catalog, tariffs, storeTariffs, products, featured, catalogRoots, catalogShortcuts, catalogCategories, } = catalogRefs; const catalogSnapshot = forwardedState(catalogRefs.catalogSnapshot); const tariffsSnapshot = forwardedState(catalogRefs.tariffsSnapshot); const { setCatalogTree, setTariffs, setStoreTariffs, getTariff, getCatalogCategory, getCatalogProducts, } = catalogState; const productState = useOrionplaceProducts( scope, pinia, init, products, featured, ); const productRefs = storeToRefs(productState); const { homeProductSections, counters, } = productRefs; const homeNewProductIds = forwardedState(productRefs.homeNewProductIds); const productSecondaryVariants = forwardedState( productRefs.productSecondaryVariants, ); const productUpdates = forwardedState(productRefs.productUpdates); const productRequests = forwardedState(productRefs.productRequests); const { setHomeNewProducts, getProducts, getProduct, setProduct, patchProductState, getProductRequest, setProductRequest, getSecondaryVariants, synchronizeProductCart, setSecondaryVariants, getProductAvailability, setProductCounters, } = productState; const browse = useOrionplaceBrowse( scope, pinia, init, ); const browseRefs = storeToRefs(browse); const searchQuery = forwardedState(browseRefs.searchQuery); const searchFacets = forwardedState(browseRefs.searchFacets); const searchFilters = forwardedState(browseRefs.searchFilters); const blogPosts = forwardedState(browseRefs.blogPosts); const { setSearchFacets, preserveSearchFacets, setSearchFilters, setBlogPosts, } = browse; registerOrionplaceDomainStores(pinia, scope, { selections, catalog: catalogState, products: productState, browse, }); function patchProfile(value: Partial<IApploadUserProfile>) { const current = result.value; const page = current?.response.page_data; if (!current || !page?.user || !isAuthenticated.value) { return; } result.value = { ...current, response: { ...current.response, page_data: { ...page, user: { ...page.user, ...value } }, }, }; } function patchWallet( wallet: NonNullable<NonNullable<typeof init.value>['wallet']>, ) { const current = result.value; const page = current?.response.page_data; if (!current || !page?.init || !isAuthenticated.value) { return; } result.value = { ...current, response: { ...current.response, page_data: { ...page, init: { ...page.init, wallet } }, }, }; } function applyResult(value: AppLoadResult) { catalogState.reset(); productState.reset(); browse.resetResults(); result.value = value; status.value = 'success'; failure.value = null; selections.reconcile(); } const publicEndpoints = computed(() => data.value?.unauth.paths ?? {}); const authenticatedEndpoints = computed(() => isAuthenticated.value ? (data.value?.auth?.paths ?? {}) : {}, ); const endpoints = computed<IAppLoadPathObject>(() => { const merged: IAppLoadPathObject = {}; for (const groups of [ publicEndpoints.value, authenticatedEndpoints.value, ]) { for (const [section, paths] of Object.entries(groups)) { merged[section] = { ...merged[section], ...paths }; } } return merged; }); const getEndpoint = (section: string, name: string) => endpoints.value[section]?.[name]; // These belong to this store instance, never serialized state or a module-level cache. let pending: Promise<void> | null = null; let controller: AbortController | null = null; let generation = 0; function reset() { generation++; controller?.abort(); controller = null; pending = null; result.value = null; catalogState.reset(); productState.reset(); browse.reset(); selections.reset(); failure.value = null; status.value = 'idle'; } /* * The shared API client calls this when a request answers HTTP 401 while * this store still shows a signed-in customer. The signed-in data is removed * at once, so the header and account pages show the guest state, and the * guest Application Load replaces the remaining snapshot in the background. * A guest store is left unchanged. */ function endSession(loader: AppLoadLoader): Promise<void> { if (!isAuthenticated.value || !result.value) { return pending ?? Promise.resolve(); } showGuestSnapshot(result.value); return load(loader, true, true); } function showGuestSnapshot(current: AppLoadResult) { const guestProducts = (items: Record<number, IProduct>) => Object.fromEntries( Object.entries(items).map(([id, item]) => [id, guestProduct(item)]), ); generation++; controller?.abort(); controller = null; pending = null; result.value = guestAppLoadResult(current); productState.clearSession(); productUpdates.value = { products: guestProducts(productUpdates.value.products), featured: guestProducts(productUpdates.value.featured), }; for (const state of Object.values(productSecondaryVariants.value)) { for (const variant of state.data?.variants ?? []) { variant.product = guestProduct(variant.product); } } } /* * `preserve` keeps the current snapshot visible while the replacement loads. * It is used only after `endSession` has already removed signed-in data. */ function load( loader: AppLoadLoader, force = false, preserve = false, ): Promise<void> { if (pending) { return pending; } // Reuse both SSR success and failure on hydration/navigation; retry is explicit. if (!force && status.value !== 'idle') { return Promise.resolve(); } const current = ++generation; controller = new AbortController(); const signal = controller.signal; if (!preserve) { catalogState.reset(); productState.reset(); browse.resetResults(); result.value = null; failure.value = null; status.value = 'pending'; } pending = Promise.resolve() .then(() => loader(signal)) .then((value) => { if (current !== generation) { return; } applyResult(value); }) .catch((error: unknown) => { if (current !== generation) { return; } const detail = error && typeof error === 'object' && 'data' in error ? error.data : null; const known = detail && typeof detail === 'object' && 'kind' in detail && ['http', 'validation', 'invalid-response', 'network'].includes( String(detail.kind), ); failure.value = known ? { kind: detail.kind as AppLoadFailure['kind'], httpStatus: 'httpStatus' in detail && typeof detail.httpStatus === 'number' ? detail.httpStatus : null, fields: 'fields' in detail && Array.isArray(detail.fields) ? detail.fields.filter( (field): field is string => typeof field === 'string', ) : [], } : { kind: 'network', httpStatus: null, fields: [] }; result.value = null; status.value = 'error'; }) .finally(() => { if (current !== generation) { return; } pending = null; controller = null; }); return pending; } function disposeChildren() { selections.$dispose(); catalogState.$dispose(); productState.$dispose(); browse.$dispose(); } return { result, status, failure, isReady, data, init, profile, patchProfile, patchWallet, isAuthenticated, locales, currencies, catalog, catalogSnapshot, setCatalogTree, catalogRoots, catalogShortcuts, catalogCategories, products, tariffs, tariffsSnapshot, setTariffs, storeTariffs, setStoreTariffs, featured, homeNewProductIds, homeProductSections, setHomeNewProducts, weights, lengths, localeCode, currencyId, weightId, lengthId, searchQuery, searchFacets, searchFilters, blogPosts, setSearchFacets, preserveSearchFacets, setSearchFilters, setBlogPosts, currentLocale, currentCurrency, setLocale, setCurrency, setUnit, getProducts, getTariff, getProduct, productUpdates, productRequests, getProductRequest, setProductRequest, counters, setProductCounters, setProduct, patchProductState, synchronizeProductCart, productSecondaryVariants, getSecondaryVariants, setSecondaryVariants, getProductAvailability, getCatalogCategory, getCatalogProducts, applyResult, publicEndpoints, authenticatedEndpoints, endpoints, getEndpoint, load, endSession, reset, disposeChildren, }; } type OrionplaceStore = ReturnType<typeof createOrionplaceStore>; const storeScope: InjectionKey<OrionplaceStore> = Symbol('orionplace:store'); const wrappedStores = new WeakSet<object>(); // Scoped previews use another instance in the same Pinia, never another global app. export function provideOrionplaceStore(store: OrionplaceStore) { provide(storeScope, store); } export function createOrionplaceStore(id: string, pinia: Pinia) { const store = defineStore(id, () => createStoreState(id, pinia))(pinia); attachOrionplaceDomainStores(store, pinia, id); if (!wrappedStores.has(store)) { const dispose = store.$dispose.bind(store); store.$dispose = () => { store.disposeChildren(); dispose(); releaseOrionplaceDomainStores(store, pinia, id); for (const key of [ id, `${id}:selections`, `${id}:catalog`, `${id}:products`, `${id}:browse`, ]) { delete pinia.state.value[key]; } }; wrappedStores.add(store); } return store; } export const useOrionplaceStore = Object.assign( (pinia?: Pinia | null) => { const scoped = hasInjectionContext() ? inject(storeScope, null) : null; if (pinia) { return createOrionplaceStore('orionplace', pinia); } if (scoped) { return scoped; } return createOrionplaceStore('orionplace', getActivePinia()!); }, { $id: 'orionplace' }, ); registerOrionplaceDomainResolver( () => useOrionplaceStore(), );app/stores/orionplaceSelections.tsKeeps the selected language, currency, weight unit and length unit. A locale change selects its configured defaults; an Application Load refresh preserves choices that remain available.
Source code
import { computed, ref } from 'vue'; import type { ComputedRef } from 'vue'; import { defineStore } from 'pinia'; import type { Pinia } from 'pinia'; import { resolveOrionplaceDomain } from '@/stores/orionplaceDomainAccess'; import '@/stores/orionplace'; import type { IAppLoadCommon } from '@/interfaces/orionplace/appBase/IAppLoadCommon'; /** Customer choices shared by the storefront's language, currency and unit controls. */ export function useOrionplaceSelections( scope: string, pinia: Pinia, init: ComputedRef<IAppLoadCommon | null>, ) { return defineStore(`${scope}:selections`, () => { const localeCode = ref(''); const currencyId = ref(''); const weightId = ref(''); const lengthId = ref(''); const locales = computed(() => init.value?.locale ?? {}); const currencies = computed(() => init.value?.currencies ?? {}); const weights = computed(() => init.value?.weights ?? {}); const lengths = computed(() => init.value?.lengths ?? {}); const currentLocale = computed(() => findLocale(localeCode.value)?.[1]); const currentCurrency = computed(() => currencies.value[currencyId.value]); function findLocale(code: string) { return Object.entries(locales.value).find( ([key, value]) => (value.code?.trim() || key).toLowerCase() === code.trim().toLowerCase(), ); } function unitId(values: Record<string, unknown>, preferred?: number | string) { const id = String(preferred ?? ''); return Object.hasOwn(values, id) ? id : (Object.keys(values)[0] ?? ''); } function availableCurrency(preferred?: number | string) { const id = String(preferred ?? ''); if (currencies.value[id]?.allowed_select) { return id; } return Object.keys(currencies.value).find( (key) => currencies.value[key]?.allowed_select, ) ?? ''; } function setLocale(code: string) { const entry = findLocale(code); if (!entry) { return; } localeCode.value = entry[1].code?.trim() || entry[0]; currencyId.value = availableCurrency(entry[1].currency); weightId.value = unitId(weights.value, entry[1].weight); lengthId.value = unitId(lengths.value, entry[1].length); } function setCurrency(id: string) { if (currencies.value[id]?.allowed_select) { currencyId.value = id; } } function setUnit(kind: 'weights' | 'lengths', id: string) { const values = kind === 'weights' ? weights.value : lengths.value; if (!Object.hasOwn(values, id)) { return; } if (kind === 'weights') { weightId.value = id; } else { lengthId.value = id; } } function reconcile() { const selected = findLocale(localeCode.value) ?? Object.entries(locales.value)[0]; localeCode.value = selected?.[1].code?.trim() || selected?.[0] || ''; currencyId.value = availableCurrency( currencies.value[currencyId.value]?.allowed_select ? currencyId.value : selected?.[1].currency, ); weightId.value = unitId( weights.value, Object.hasOwn(weights.value, weightId.value) ? weightId.value : selected?.[1].weight, ); lengthId.value = unitId( lengths.value, Object.hasOwn(lengths.value, lengthId.value) ? lengthId.value : selected?.[1].length, ); } function reset() { localeCode.value = ''; currencyId.value = ''; weightId.value = ''; lengthId.value = ''; } return { localeCode, currencyId, weightId, lengthId, locales, currencies, weights, lengths, currentLocale, currentCurrency, setLocale, setCurrency, setUnit, reconcile, reset, }; })(pinia); } /** Access the selection store associated with the current storefront scope. */ export function useOrionplaceSelectionsStore() { return resolveOrionplaceDomain<ReturnType<typeof useOrionplaceSelections>>( 'selections', ); }app/stores/orionplaceCatalog.tsReads the initial category, product and tariff collections from Application Load. Successful catalog and tariff requests can replace their respective lists until the next refresh.
Source code
import { computed, shallowRef } from 'vue'; import type { ComputedRef } from 'vue'; import { defineStore } from 'pinia'; import type { Pinia } from 'pinia'; import { resolveOrionplaceDomain } from '@/stores/orionplaceDomainAccess'; import '@/stores/orionplace'; import type { IAppLoadCommon } from '@/interfaces/orionplace/appBase/IAppLoadCommon'; import { navigationCatalogCategories, findCatalogCategory, catalogRepresentatives, } from '@/utils/orionplace/catalog'; import { resolveAppLoadProducts } from '@/utils/orionplace/productSeller'; /** * Application Load collections and the later catalog and tariff responses. * `storeTariffs` is every plan the storefront sells; the subscriptions page * and its dialogs read it. `tariffs` is what the plan pages show: the same * list, or a search result or one plan set with `setTariffs`. * `usedSubscriptions` holds the subscription records the signed-in customer * already had, for the profile that loaded them; a new Application Load * keeps it, another profile ignores it. */ export function useOrionplaceCatalog( scope: string, pinia: Pinia, init: ComputedRef<IAppLoadCommon | null>, ) { return defineStore(`${scope}:catalog`, () => { const catalogSnapshot = shallowRef<NonNullable<IAppLoadCommon['catalog']>>(); const tariffsSnapshot = shallowRef<NonNullable<IAppLoadCommon['tariffs']>>(); const storeTariffsSnapshot = shallowRef<NonNullable<IAppLoadCommon['tariffs']>>(); const usedSubscriptions = shallowRef<{ profile: number; ids: number[] }>(); const catalog = computed( () => catalogSnapshot.value ?? init.value?.catalog ?? [], ); const storeTariffs = computed( () => storeTariffsSnapshot.value ?? init.value?.tariffs ?? null, ); const tariffs = computed( () => tariffsSnapshot.value ?? storeTariffs.value, ); const products = computed(() => resolveAppLoadProducts(init.value?.products, init.value?.sellers), ); const featured = computed(() => resolveAppLoadProducts(init.value?.featured, init.value?.sellers), ); const catalogRoots = computed(() => navigationCatalogCategories(catalog.value)); const catalogShortcuts = computed(() => catalogRoots.value.filter((category) => category.separate_menu), ); const catalogCategories = computed(() => { const categories = catalogRoots.value.filter( (category) => !category.separate_menu, ); return categories.length ? categories : catalogRoots.value; }); function setCatalogTree(tree: NonNullable<IAppLoadCommon['catalog']>) { catalogSnapshot.value = tree; } /** A search result or one plan: only the plan pages show it. */ function setTariffs(value: NonNullable<IAppLoadCommon['tariffs']>) { tariffsSnapshot.value = value; } /** The complete plan list of the storefront, loaded after Application Load. */ function setStoreTariffs(value: NonNullable<IAppLoadCommon['tariffs']>) { storeTariffsSnapshot.value = value; tariffsSnapshot.value = undefined; } /** The records the customer had, see `tariffUsedSubscriptions`. */ function setUsedSubscriptions(profile: number, ids: number[]) { usedSubscriptions.value = { profile, ids }; } /** Those records for this profile; undefined until they are loaded. */ function usedSubscriptionsOf(profile: number | undefined) { return usedSubscriptions.value && usedSubscriptions.value.profile === profile ? usedSubscriptions.value.ids : undefined; } function getTariff(id: number) { return tariffs.value?.data.find((tariff) => tariff.id === id); } function getCatalogCategory(id: number) { return findCatalogCategory(catalog.value, id); } function getCatalogProducts(categoryId: number) { return catalogRepresentatives( getCatalogCategory(categoryId), products.value, featured.value, ); } function reset() { catalogSnapshot.value = undefined; tariffsSnapshot.value = undefined; storeTariffsSnapshot.value = undefined; } return { catalogSnapshot, tariffsSnapshot, storeTariffsSnapshot, usedSubscriptions, catalog, tariffs, storeTariffs, products, featured, catalogRoots, catalogShortcuts, catalogCategories, setCatalogTree, setTariffs, setStoreTariffs, setUsedSubscriptions, usedSubscriptionsOf, getTariff, getCatalogCategory, getCatalogProducts, reset, }; })(pinia); } /** Access the catalog store associated with the current storefront scope. */ export function useOrionplaceCatalogStore() { return resolveOrionplaceDomain<ReturnType<typeof useOrionplaceCatalog>>( 'catalog', ); }app/stores/orionplaceProducts.tsKeeps product changes, secondary variants, home product groups and cart and wishlist counters. Confirmed actions update these values; Application Load refresh clears them.
Source code
import { computed, ref } from 'vue'; import type { ComputedRef } from 'vue'; import { defineStore } from 'pinia'; import type { Pinia } from 'pinia'; import { resolveOrionplaceDomain } from '@/stores/orionplaceDomainAccess'; import '@/stores/orionplace'; import type { IAppLoadCommon } from '@/interfaces/orionplace/appBase/IAppLoadCommon'; import type { IProduct, IProductSearchSeller, } from '@/interfaces/orionplace/products/IProduct'; import type { IProductSecondaryVariantState } from '@/interfaces/orionplace/products/IProductSecondaryVariantRequest'; import { homeProductSections as groupHomeProducts } from '@/utils/orionplace/homeContent'; import { productAvailability } from '@/utils/orionplace/productAvailability'; import { productWithSellerPermission } from '@/utils/orionplace/productSeller'; /** Products shared by cards, cart actions and variant selectors. */ export function useOrionplaceProducts( scope: string, pinia: Pinia, init: ComputedRef<IAppLoadCommon | null>, products: ComputedRef<IProduct[]>, featured: ComputedRef<IProduct[]>, ) { return defineStore(`${scope}:products`, () => { const homeNewProductIds = ref<number[] | null>(null); const homeProductSections = computed(() => groupHomeProducts(products.value, featured.value, homeNewProductIds.value), ); function setHomeNewProducts( items: IProduct[], sellers: IProductSearchSeller[] = [], ) { if (!init.value) { return; } for (const product of items) { setProduct(product, 'products', sellers); } homeNewProductIds.value = items.map((product) => product.id); } const productSecondaryVariants = ref< Record<string, IProductSecondaryVariantState> >({}); const productCounters = ref<{ c_cart_positions?: number; c_wishlist?: number; }>({}); const counters = computed(() => ({ ...init.value?.variables, ...productCounters.value, })); function setProductCounters(values: { c_cart_positions?: number; c_wishlist?: number; }) { productCounters.value = { ...productCounters.value, ...values }; } const productUpdates = ref<{ products: Record<number, IProduct>; featured: Record<number, IProduct>; }>({ products: {}, featured: {} }); // Variant targets the host is loading for a card; see useOrionplaceProductVariants. const productRequests = ref< Record<number, 'loading' | 'failed'> >({}); function getProducts(collection: 'products' | 'featured' = 'products') { return collection === 'featured' ? featured.value : products.value; } function getProduct( id: number, collection: 'products' | 'featured' = 'products', ) { return ( productUpdates.value[collection][id] ?? getProducts(collection).find((product) => product.id === id) ); } function setProduct( product: IProduct, collection: 'products' | 'featured' = 'products', sellers: IProductSearchSeller[] = [], ) { if (init.value && Number.isInteger(product.id) && product.id > 0) { productUpdates.value[collection][product.id] = productWithSellerPermission(product, sellers); } } function patchProductState( id: number, patch: Partial< Pick< IProduct, | 'cart_counts' | 'whishlist_id' | 'subscription_id' | 'c_real_left' | 'seller' > >, ) { for (const collection of ['products', 'featured'] as const) { const product = getProduct(id, collection); if (product) { setProduct({ ...product, ...patch }, collection); } } for (const state of Object.values(productSecondaryVariants.value)) { for (const variant of state.data?.variants ?? []) { if (variant.product.id === id) { variant.product = { ...variant.product, ...patch }; } } } } function getProductRequest(id: number) { return productRequests.value[id]; } function setProductRequest(id: number, state?: 'loading' | 'failed') { if (!Number.isInteger(id) || id <= 0) { return; } if (state) { productRequests.value[id] = state; } else { delete productRequests.value[id]; } } function getSecondaryVariants( productGroup: number, mainVariantValue: number, ) { return productSecondaryVariants.value[ `${productGroup}:${mainVariantValue}` ]; } function synchronizeProductCart(items: IProduct[]) { const counts = new Map( items.map((item) => [item.id, item.cart_counts ?? null]), ); for (const collection of ['products', 'featured'] as const) { const cached = new Map([ ...getProducts(collection).map((item) => [item.id, item] as const), ...Object.values(productUpdates.value[collection]).map( (item) => [item.id, item] as const, ), ]); for (const product of cached.values()) { productUpdates.value[collection][product.id] = { ...product, cart_counts: counts.get(product.id) ?? null, }; } } for (const state of Object.values(productSecondaryVariants.value)) { for (const variant of state.data?.variants ?? []) { variant.product = { ...variant.product, cart_counts: counts.get(variant.product.id) ?? null, }; } } } function setSecondaryVariants( productGroup: number, mainVariantValue: number, state: IProductSecondaryVariantState, ) { if ( init.value && Number.isInteger(productGroup) && productGroup > 0 && Number.isInteger(mainVariantValue) && mainVariantValue > 0 ) { productSecondaryVariants.value[`${productGroup}:${mainVariantValue}`] = state; } } function getProductAvailability( id: number, collection: 'products' | 'featured' = 'products', ) { const product = getProduct(id, collection); return productAvailability(product, init.value?.variables?.allow_preorder); } function resetHome() { homeNewProductIds.value = null; } function clearSession() { productRequests.value = {}; productCounters.value = {}; } function reset() { resetHome(); productSecondaryVariants.value = {}; productUpdates.value = { products: {}, featured: {} }; clearSession(); } return { homeNewProductIds, homeProductSections, setHomeNewProducts, productSecondaryVariants, productCounters, counters, setProductCounters, productUpdates, productRequests, getProducts, getProduct, setProduct, patchProductState, getProductRequest, setProductRequest, getSecondaryVariants, synchronizeProductCart, setSecondaryVariants, getProductAvailability, clearSession, reset, }; })(pinia); } /** Access the product store associated with the current storefront scope. */ export function useOrionplaceProductsStore() { return resolveOrionplaceDomain<ReturnType<typeof useOrionplaceProducts>>( 'products', ); }app/stores/orionplaceBrowse.tsKeeps the search query, selected filters, search choices and journal posts. Search and journal requests fill these values; Application Load refresh clears results while keeping the query.
Source code
import { ref, shallowRef } from 'vue'; import type { ComputedRef } from 'vue'; import { defineStore } from 'pinia'; import type { Pinia } from 'pinia'; import { resolveOrionplaceDomain } from '@/stores/orionplaceDomainAccess'; import '@/stores/orionplace'; import type { IAppLoadCommon } from '@/interfaces/orionplace/appBase/IAppLoadCommon'; import type { IBlogPostPreview } from '@/interfaces/orionplace/blog/IBlogPostPreview'; import type { IProductSearchFacets, ProductFacetSelection, } from '@/interfaces/orionplace/products/IProductSearchFacets'; import { cloneSearchFilters, initialSearchFacets, } from '@/utils/orionplace/searchFilters'; /** Search choices and journal results loaded after Application Load. */ export function useOrionplaceBrowse( scope: string, pinia: Pinia, init: ComputedRef<IAppLoadCommon | null>, ) { return defineStore(`${scope}:browse`, () => { const searchQuery = ref(''); const searchFacets = shallowRef<IProductSearchFacets | null>(null); const searchFilters = ref<ProductFacetSelection>({}); const blogPosts = shallowRef<IBlogPostPreview[]>([]); function setSearchFacets(value: IProductSearchFacets | null) { searchFacets.value = value; } function preserveSearchFacets(value: IProductSearchFacets) { searchFacets.value = initialSearchFacets(searchFacets.value, value); } function setSearchFilters(value: ProductFacetSelection) { const selected = cloneSearchFilters(value); if (init.value?.visual_settings?.single_seller_mode) { delete selected.product_owner; } searchFilters.value = selected; } function setBlogPosts(value: IBlogPostPreview[]) { blogPosts.value = [...value]; } function resetResults() { searchFacets.value = null; searchFilters.value = {}; blogPosts.value = []; } function reset() { resetResults(); searchQuery.value = ''; } return { searchQuery, searchFacets, searchFilters, blogPosts, setSearchFacets, preserveSearchFacets, setSearchFilters, setBlogPosts, resetResults, reset, }; })(pinia); } /** Access the browse store associated with the current storefront scope. */ export function useOrionplaceBrowseStore() { return resolveOrionplaceDomain<ReturnType<typeof useOrionplaceBrowse>>('browse'); }app/stores/orionplaceDomainAccess.tsConnects each feature-store hook to the current storefront or documentation preview. It keeps server requests and preview data separate.
Source code
import type { Pinia } from 'pinia'; export type OrionplaceDomain = 'selections' | 'catalog' | 'products' | 'browse'; type DomainStores = Record<OrionplaceDomain, unknown>; const storesByScope = new WeakMap<Pinia, Map<string, DomainStores>>(); const storesByFacade = new WeakMap<object, DomainStores>(); let resolveFacade: (() => object) | undefined; export function registerOrionplaceDomainResolver(resolver: () => object) { resolveFacade = resolver; } export function registerOrionplaceDomainStores( pinia: Pinia, scope: string, stores: DomainStores, ) { const scopes = storesByScope.get(pinia) ?? new Map<string, DomainStores>(); scopes.set(scope, stores); storesByScope.set(pinia, scopes); } export function attachOrionplaceDomainStores( facade: object, pinia: Pinia, scope: string, ) { const stores = storesByScope.get(pinia)?.get(scope); if (!stores) { throw new Error(`Missing Orionplace stores for ${scope}.`); } storesByFacade.set(facade, stores); } export function releaseOrionplaceDomainStores( facade: object, pinia: Pinia, scope: string, ) { storesByFacade.delete(facade); storesByScope.get(pinia)?.delete(scope); } export function resolveOrionplaceDomain<T>(domain: OrionplaceDomain): T { const facade = resolveFacade?.(); const stores = facade && storesByFacade.get(facade); if (!stores) { throw new Error('The Orionplace store has not been initialized.'); } return stores[domain] as T; }app/interfaces/orionplace/appBase/IAppLoad.tsIAppLoaddescribes init, unauth, user and auth.Nested types are imported from neighboring files; common init fields are defined in app/interfaces/orionplace/appBase/IAppLoadCommon.ts.
app/interfaces/orionplace/appBase/IAppLoadCommon.tsIAppLoadCommonlists init fields and their nested interfaces: languages, currencies, units, banners, catalog and other initial data.Source code
Shared storefront settings and initial content.
app/interfaces/orionplace/appBase/IAppLoadCommon.tsimport type { IAppLoadCommonLocale } from '@/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonLocale'; import type { IAppLoadCommonCurrency } from '@/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonCurrency'; import type { IAppLoadCommonUnit } from '@/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonUnit'; import type { IAppLoadCommonLogos } from '@/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonLogos'; import type { IGeoCoordinates } from '@/interfaces/orionplace/shared/IGeoCoordinates'; import type { IAppLoadCommonSeo } from '@/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonSEO'; import type { IAppLoadCommonSocial } from '@/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonSocial'; import type { IAppLoadCommonVisualSettings } from '@/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonVisualSettings'; import type { IAppLoadCommonEvents } from '@/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonEvents'; import type { IAppLoadCommonBanner } from '@/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonBanner'; import type { ICategory } from '@/interfaces/orionplace/category/ICategory'; import type { IAppLoadProduct, IProductSeller, } from '@/interfaces/orionplace/products/IProduct'; import type { IAppLoadCommonTariff } from '@/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonTariff'; import type { IAppLoadCommonWalletValue } from '@/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonWalletValue'; import type { IAppLoadCommonVariables } from '@/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonVariables'; import type { IAppLoadCommonContact } from '@/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonContact'; export interface IAppLoadCommon { // Available languages and their default currency and units. locale: Record<string, IAppLoadCommonLocale>; // Currencies, formatting rules, conversion values and selector availability. currencies: Record<string, IAppLoadCommonCurrency>; // Weight units and conversion factors. weights: Record<string, IAppLoadCommonUnit>; // Length units and conversion factors. lengths: Record<string, IAppLoadCommonUnit>; // Whether a geocoding plugin is selected for address lookup by coordinates. // Address and city text suggestions depend on the country. geo_location_enabled: boolean; /** ISO codes of the shop's shipping countries, sorted. */ shipping_countries?: string[]; // Whether the current session belongs to a signed-in customer. is_authenticated: boolean; // Customer balance by currency, when a wallet is available. wallet?: Record<string, IAppLoadCommonWalletValue>; // Logo sizes and favicon. logos: IAppLoadCommonLogos; // Public keys for external integrations, including the Yandex Maps API key. public_keys: Record<string, string>; // Default map center coordinates configured for the storefront. default_map: IGeoCoordinates; // Store home-page metadata seo?: IAppLoadCommonSeo[]; // Social network links. sm?: IAppLoadCommonSocial[]; // Initial cart and wishlist counters and feature defaults. variables?: IAppLoadCommonVariables; // Company name, contact details and legal information. contact?: IAppLoadCommonContact; // Display options for menus, lists and storefront elements. visual_settings?: IAppLoadCommonVisualSettings; // Events and messages displayed in the storefront. events?: IAppLoadCommonEvents[]; // Banner collections for the homepage. main_banner?: Record<string, IAppLoadCommonBanner[]>; // Category tree for storefront navigation. catalog?: ICategory[]; // Initial storefront products. Each entry contains `seller_id`; read the full // seller from `sellers`. products?: IAppLoadProduct[]; // Personalized products based on previous searches and categories. Each entry // contains `seller_id`; read the full seller from `sellers`. Empty for guests. featured?: IAppLoadProduct[]; // Seller details keyed by seller ID. Product stores use this object to provide // `IProduct.seller` to components. sellers: Record<string, IProductSeller>; // Localized suggestions for the search input. search_hints?: Record<string, string>[]; // Plans this storefront sells, with their prices and options, as in // `subscriptions_guest_search`. A storefront without a plan list gets an empty // `data` list. tariffs?: IAppLoadCommonTariff; }app/app.vueThe root component applies the selected theme with
OrionplaceThemeProviderand renders the current route page withNuxtPageinsideNuxtLayout.NuxtLoadingIndicatordraws a thin bar at the top of the window in the header text color, so it stays visible over the dark header bar, and in the danger color after an error. It runs during page navigation and while a storefront request is in progress: saving, updating, loading and signing in. The buttons stay disabled and screen readers receive the status text; the page shows no separate status strip.This connects the route pages, the layouts and the global route middleware to the application lifecycle.
Source code
<script setup lang="ts"> import { watch } from 'vue'; import { useOrionplaceStore } from '@/stores/orionplace'; import { NuxtLayout, NuxtLoadingIndicator, NuxtPage } from '#components'; import { showError, useHead, useRoute } from '#imports'; import OrionplaceThemeProvider from '@/components/orionplace/theme/OrionplaceThemeProvider.vue'; import { useOrionplaceTheme } from '@/composables/useOrionplaceTheme'; import { storefrontPageError } from '@/composables/useOrionplaceMissingPage'; import { storefrontIconLinks } from '@/utils/orionplace/pageSeo'; const store = useOrionplaceStore(); const route = useRoute(); const { mode } = useOrionplaceTheme(); // The store's icon from the admin panel (logos.ico) for browser tabs and // search results; a page without it keeps the browser default. useHead(() => ({ htmlAttrs: { lang: store.localeCode }, link: storefrontIconLinks(store.init?.logos?.ico), })); // The middleware opens the 503 error page during navigation. A refresh that // fails while a page is open, for example after sign-in, opens it too. watch( () => store.status, (status) => { const error = route.matched.length ? storefrontPageError(status, false, '') : undefined; if (error) { void showError(error); } }, ); </script> <template> <OrionplaceThemeProvider :mode="mode" class="min-h-screen" > <NuxtLoadingIndicator :height="2" color="var(--orionplace-inverse-text)" error-color="var(--orionplace-danger)" /> <NuxtLayout> <NuxtPage /> </NuxtLayout> </OrionplaceThemeProvider> </template>app/pages/index.vueThe starter home page reads the load status from the store. Middleware performs the first request before the page opens.
While Application Load is pending, the page shows a loading message. A failed Application Load never reaches the page: the middleware opens the storefront error page instead.
After success, it renders
StorefrontHome: banners, categories, events and product selections from Application Load, plus new products requested withproducts_search. If new products fail to load, the page shows a message with its own Retry button. Page metadata comes fromstore.init.seo. More in the Featured section opens the/featuredpage.Source code
<script setup lang="ts"> import { useOrionplacePageSeo } from '@/composables/useOrionplacePageSeo'; import { computed, defineAsyncComponent, onMounted, ref } from 'vue'; import { useHead, navigateTo, useRoute } from '#imports'; import { useOrionplaceStore } from '@/stores/orionplace'; import { useOrionplaceHomeProducts } from '@/composables/useOrionplaceHomeProducts'; import { useOrionplaceStorefront } from '@/composables/useOrionplaceStorefront'; import { useOrionplaceSignIn } from '@/composables/useOrionplaceSignIn'; import { eventReturnPath } from '@/utils/orionplace/signInReturn'; import StorefrontHome from '@/components/orionplace/storefront/StorefrontHome.vue'; import { homeLabels } from '@/utils/orionplace/homeLabels'; import { headerLabels } from '@/utils/orionplace/headerLabels'; import { getStorefrontSeo } from '@/utils/orionplace/seo'; import type { IAppLoadCommonBanner } from '@/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonBanner'; import type { IAppLoadCommonEvents } from '@/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonEvents'; const EventEnrollmentDialog = defineAsyncComponent( () => import('@/components/orionplace/events/EventEnrollmentDialog.vue'), ); const enrollment = ref<IAppLoadCommonEvents>(); function openEnrollment(event: IAppLoadCommonEvents) { if (event.promo) { enrollment.value = event; } } // A guest's click opens sign-in at once; it returns with `?event=<id>`. function selectEvent(event: IAppLoadCommonEvents) { if (event.promo && !store.isAuthenticated) { signIn.requireSignIn(eventReturnPath(route.fullPath, event.id)); return; } openEnrollment(event); } // The return address `?event=<id>` opens the dialog again for that event, // without navigating anywhere. Closing it removes the parameter. function reopenEnrollment() { const id = Number(route.query.event); const event = store.init?.events?.find((item) => item.id === id); if (event) { openEnrollment(event); } } function closeEnrollment() { enrollment.value = undefined; if (route.query.event === undefined) { return; } const query = { ...route.query }; delete query.event; return navigateTo({ path: route.path, query }, { replace: true }); } function openBannerCategory(banner: IAppLoadCommonBanner) { if (banner.category != null) { return navigateTo({ path: `/category/${banner.category}`, query: { locale: store.localeCode }, }); } } const store = useOrionplaceStore(); const route = useRoute(); const signIn = useOrionplaceSignIn(); const { actions, variants, selection: headerSelection, } = useOrionplaceStorefront(); const homeProducts = useOrionplaceHomeProducts(); const labels = computed(() => headerLabels(store.localeCode)); const homeCopy = computed(() => homeLabels(store.localeCode)); const metadata = computed(() => getStorefrontSeo(store.init?.seo ?? [], store.localeCode), ); function namedMeta(name: string) { return metadata.value.meta.findLast( (tag) => 'name' in tag && tag.name === name, )?.content; } onMounted(reopenEnrollment); useOrionplacePageSeo('home', () => store.localeCode, { title: () => metadata.value.title || store.init?.contact?.company_name || 'Orionplace', description: () => namedMeta('description'), keywords: () => namedMeta('keywords'), }); useHead(() => ({ meta: metadata.value.meta.filter( (tag) => !('name' in tag && ['description', 'keywords'].includes(tag.name)), ), })); </script> <template> <main> <h1 class="sr-only"> {{ store.init?.contact?.company_name || 'Orionplace' }} </h1> <p v-if="homeProducts.error.value" role="status" class=" flex flex-wrap items-center gap-3 mx-auto max-w-[1440px] px-4 sm:px-6 lg:px-8 text-sm text-orionplace-muted " > {{ homeCopy.newFailed }} <button type="button" class=" inline-flex h-11 items-center justify-center px-6 text-xs font-medium uppercase tracking-[0.08em] rounded-none border border-solid border-orionplace-text bg-transparent text-orionplace-text cursor-pointer outline-none hover:bg-orionplace-hover focus-visible:bg-orionplace-hover " @click="homeProducts.load()" > {{ homeCopy.retry }} </button> </p> <StorefrontHome v-if="store.status === 'success'" :product-actions-disabled=" actions.pending.value || actions.needsRefresh.value " @add="actions.run('add', $event)" @remove="actions.run('remove', $event)" @toggle-favorite="actions.run('toggleFavorite', $event)" @subscribe="actions.run('subscribe', $event)" @unsubscribe="actions.run('unsubscribe', $event)" @preorder="actions.openPreorder" @select-variant="variants.select" @request-secondary-variants="variants.requestSecondary" @select-category=" navigateTo({ path: `/category/${$event.id}`, query: { locale: store.localeCode }, }) " @select-product=" navigateTo({ path: `/product/${$event.id}`, query: { locale: store.localeCode }, }) " @select-seller=" navigateTo({ path: '/sellers', query: { seller: $event.id, locale: store.localeCode }, }) " @select-banner="openBannerCategory" @select-featured=" navigateTo({ path: '/featured', query: { locale: store.localeCode } }) " @select-event="selectEvent" /> <div v-else class=" grid justify-items-start gap-4 mx-auto max-w-[1440px] px-4 py-12 sm:px-6 lg:px-8 text-sm " :aria-busy="store.status === 'pending'" > <p class="m-0" role="status" > {{ homeCopy.loading }} </p> </div> <EventEnrollmentDialog v-if="enrollment" :key="enrollment.id" :event="enrollment" @close="closeEnrollment" /> </main> </template>
Each SSR request has its own Pinia instance.
The browser receives that state during hydration. After sign-in or sign-out, call reset and refresh to replace the profile and available methods.