Skip to main content

State & View Services

These two services manage the application's runtime state and visual orchestration. stateService holds the "who" and "what" (which user, which device, which place), while viewService holds the "where" and "how" (which screen, which theme, which panels). Together they drive the entire navigation and access-control model.

stateService

Introduction

stateService is the single source of truth for the application's authentication and identity state. It holds the current access mode (guest vs. authenticated), the session and device identifiers that authenticate every backend request, the active place (establishment), and flags for session expiry and system readiness. It does not contain any UI logic -- it is a pure data holder with change-notification observables.

When to use

  • Login/logout flows: set session, device, Access, and place after authentication.
  • Session expiry handling: subscribe to OnExpired to redirect to the login screen.
  • System readiness: set IsReady = true when all initial data is loaded; other services (like preloadService) wait for this signal.
  • Backend requests: adhocService reads device, session, place from stateService on every call -- you do not need to pass these manually.

How it works

stateService is a simple reactive state container. Each piece of state has a private backing field, a public getter/setter, and (for the important ones) a Subject that emits when the value changes. The values for session, device, and place are stored URL-encoded, ready to be appended to query strings by adhocService.

The service is stateless across page reloads -- it does not persist anything to storage. Persistence of the session across reloads is handled by the login flow, which re-authenticates or recovers the session from storeService.

import { stateService, UserMode, UserSession } from '@unpispas/upp-base';

constructor(private state: stateService) {}

Types

type UserMode = 'GUEST' | 'LOGIN';

type UserSession = {
session: string | null;
device: string | null;
};
  • GUEST: anonymous access via QR code. The user scans a QR at a place and gets a limited, read-only experience.
  • LOGIN: authenticated access with username and password. Full functionality including editing, ticket creation, and administration.

Properties

PropertyTypeDescription
AccessUserMode | nullCurrent access mode. 'GUEST' for QR-scanned access, 'LOGIN' for authenticated users. null before any access mode is set.
userSessionUserSession | nullRaw session object containing both session and device identifiers.
sessionstring | nullURL-encoded session identifier. Setting a new value emits OnSession. The session is assigned by the backend after successful authentication and is required for all subsequent API calls.
devicestring | nullURL-encoded device identifier. Setting a new value emits OnDevice. Generated by identificationService.DeviceId() during the bootstrap flow.
placestring | nullURL-encoded place objid. Identifies the active establishment. Used by adhocService to scope all API calls to the correct place.
IsExpiredbooleanSession expiration flag. When the backend returns a session-expired error, the application sets this to true, which emits OnExpired and triggers the logout flow.
IsReadybooleanSystem readiness flag. Set to true once all initial data (device ID, session, language, place data) is loaded. Other services like preloadService wait for this signal before starting their work.

Observables

ObservableEmits whenTypical subscriber
OnDeviceThe device ID changes.Login flow (to confirm device registration).
OnSessionThe session ID changes.Components that need to react to login/logout.
OnExpiredThe session expires (IsExpired set to true).viewService resets the view; app navigates to login.
OnReadyThe system is ready (IsReady set to true).preloadService starts image loading; feature modules begin data sync.

Usage examples

Setting state after a successful login

// 1. Device ID is obtained during bootstrap
const deviceId = await this.identification.DeviceId();
this.state.device = deviceId;

// 2. After login API call succeeds
this.state.session = response.sessionId;
this.state.Access = 'LOGIN';
this.state.place = selectedPlace.objid;

// 3. After all initial data is loaded
this.state.IsReady = true;

Handling session expiry

// In the root app component
this.state.OnExpired.subscribe(() => {
this.state.session = null;
this.state.Access = null;
this.router.navigate(['/login']);
});

Waiting for system readiness

// In a service that needs to start work after everything is ready
if (this.state.IsReady) {
this.startSync();
} else {
this.state.OnReady.subscribe(() => this.startSync());
}

viewService

Introduction

viewService orchestrates everything the user sees: the current screen (login, user selection, place), the editing mode (view, edit, cart), panel visibility, theme, kiosk mode, online status display, and service-worker updates. It is the central coordination point for the UI, aggregating state from platformService, httpService, stateService, languageService, and configService into a single reactive surface that components subscribe to.

When to use

  • Navigation: change View to switch between LOGIN, USER, PLACE, and GUEST screens.
  • Mode switching: change Mode to switch between VIEW (read-only), EDIT (configuration), and CART (ticket creation).
  • Panel management: use DefaultPanel, MainTab, ScndTab, and PanelArg to control the right-side panel and tab navigation.
  • Theme toggling: set Theme to switch between dark and light modes.
  • Close-request protocol: use GrantViewChange() before navigating away from a screen to give open panels a chance to save or cancel unsaved changes.
  • Connectivity display: bind UI indicators to IsOnline and OnOnline.

How it works

viewService acts as a facade that combines multiple lower-level services:

  platformService ──► theme, kiosk, mobile/desktop detection
httpService ──────► online/offline status
stateService ─────► access mode, session expiry
languageService ──► language loading triggers view refresh
configService ────► persisted VIEW preferences (kiosk, ispos)


viewService ────► single OnViewChanged observable for the UI

When any of these inputs change, viewService emits OnViewChanged, which is the primary observable that components subscribe to for refreshing their state. This means components do not need to subscribe to five different services -- they subscribe to viewService.OnViewChanged and rebuild their view.

import { viewService, ViewMode, EditMode, PlacePanel, UppViewConfiguration } from '@unpispas/upp-base';

constructor(private view: viewService) {}

Types

type ViewMode = 'LOGIN' | 'USER' | 'PLACE' | 'GUEST';
type EditMode = 'VIEW' | 'EDIT' | 'CART';

type PlacePanel = {
main?: string | null; // primary panel identifier
scnd?: string | null; // secondary panel identifier
args?: Record<string, any> | null; // arguments for the secondary panel
sync?: ((...args: any[]) => any) | null; // sync callback
};

enum UppViewConfiguration {
kiosk = 'kiosk', // virtual keyboard mode
ispos = 'ispos' // POS-specific behaviour
}

ViewMode values:

  • LOGIN: the login form is displayed.
  • USER: the user has logged in and is selecting a place.
  • PLACE: the user is inside a place -- the main working view.
  • GUEST: the user accessed via QR code -- a restricted PLACE view.

EditMode values (only relevant when View is PLACE):

  • VIEW: read-only view of the place's activity (tickets, orders).
  • EDIT: configuration mode for the place (products, settings).
  • CART: ticket creation mode -- the user is building an order.

View and access control

PropertyTypeDescription
ViewViewMode | nullCurrent screen. Setting a new value emits OnViewChanged. Set to null to indicate no active view (e.g. after session expiry).
AccessUserMode | nullDelegates to stateService.Access. The setter also sets the View: setting 'GUEST' switches to PLACE view; 'LOGIN' switches to LOGIN view. This coupling ensures the access mode and view are always consistent.
ModeEditMode | nullCurrent edit mode within the PLACE view (VIEW, EDIT, or CART). Setting a new value emits OnViewChanged.

Device and display

PropertyTypeDescription
CanZoombooleanfalse on iOS Safari (stability issues), true otherwise. Use this to conditionally show zoom controls.
Theme'dark' | 'light' | nullCurrent theme. Setting it delegates to platformService._theme, which adds/removes the CSS class on document.body. Guests are locked to 'dark'.
LightTheme / DarkThemebooleanConvenience getters for template bindings.
ScrollbarbooleanScrollbar visibility. Delegates to platformService._scrollbar.
Mobile / DesktopbooleanDevice category. Delegates to platformService._isMobile / _isDesktop.
Legacybooleantrue for legacy mobile (phones) or legacy desktop devices. Used to adapt layouts for devices with native keyboards vs. touch-only devices.
KioskbooleanVirtual-keyboard kiosk mode. When true, the <upp-kiosk-board> component shows an on-screen keyboard. Persisted via configService so the setting survives page reloads.
IsPOSbooleanPOS-specific behaviour flag. Persisted via configService. When true, enables Point of Sale optimisations.

Panels and tabs

Property / MethodTypeDescription
DefaultPanelstring | nullThe default right-side panel identifier. Emits OnRightChanged when changed.
MainTabstring | nullPrimary tab for the current EditMode. Each mode has its own stored tab, so switching modes restores the last-active tab.
SetMainTab(mode, value)voidSets the primary tab for a specific mode. Emits OnTabChanged if the mode is the current one.
ScndTabstring | nullSecondary tab within the current mode.
PanelArgany | nullArguments for the secondary panel. Setting this emits OnTabChanged.
PanelPlacePanel | nullCurrent place panel configuration. Used for complex panel setups where main/secondary/sync need to be set together.

Close-request protocol

The close-request protocol is a collaborative mechanism that allows open panels to prevent navigation if they have unsaved changes.

MemberDescription
OnCloseRequestCustom subscribable (not a standard Observable). Subscribers receive close requests and must respond via OnCloseResponse. The subscriber count is tracked internally.
OnCloseResponse(allow)Panels call this with true (allow navigation) or false (block navigation).
GrantViewChange()Returns Promise<boolean> -- resolves to true only if every subscriber allowed the close. If there are no subscribers, resolves to true immediately.

How to use it:

// Before navigating away
const canLeave = await this.view.GrantViewChange();
if (canLeave) {
this.view.View = 'USER';
}
// In a panel component with unsaved changes
ngOnInit() {
this.closeSub = this.view.OnCloseRequest.subscribe(() => {
if (this.hasUnsavedChanges) {
// Ask the user or just block
this.view.OnCloseResponse(false);
} else {
this.view.OnCloseResponse(true);
}
});
}

ngOnDestroy() {
this.closeSub.unsubscribe();
}

Online and service worker

PropertyTypeDescription
IsOnlinebooleanDelegates to httpService.IsOnline. Prefer using this over injecting httpService directly in components.
OnOnlineObservable<boolean>Delegates to httpService.OnOnline.
SwUpdatebooleanSet to true when a new service-worker version is detected. Emits OnViewChanged so the UI can show an update banner.

Observables

ObservableEmits whenTypical use
OnViewChangedView, mode, SW update, or language changes.Main refresh trigger for components.
OnRightChangedThe default right panel changes.Right-panel container listens to show/hide panels.
OnTabChangedA tab changes (main or secondary).Tab bar components refresh their selection state.
OnThemeThe theme changes (delegates to platformService).Components that need to adjust colours or icons.
OnKioskKiosk mode changes (delegates to platformService).Kiosk board component visibility.

Usage examples

Complete login flow

// After successful authentication
this.view.Access = 'LOGIN'; // sets stateService.Access and switches View to LOGIN

// After user selects a place
this.view.View = 'USER'; // show place selection screen
// ... user picks a place ...
this.state.place = selectedPlace.objid;
this.view.View = 'PLACE'; // enter the main working view
this.view.Mode = 'VIEW'; // start in read-only mode

Switching between edit modes

// Toggle between view and edit mode
onEditToggle() {
this.view.Mode = this.view.Mode === 'VIEW' ? 'EDIT' : 'VIEW';
}

// Enter cart (ticket creation) mode
onNewTicket() {
this.view.Mode = 'CART';
}

Theme toggling

onThemeToggle() {
this.view.Theme = this.view.DarkTheme ? 'light' : 'dark';
}

Reacting to any view change

ngOnInit() {
this.view.OnViewChanged.subscribe(() => {
this.isPlaceView = this.view.View === 'PLACE';
this.isEditing = this.view.Mode === 'EDIT';
this.showUpdateBanner = this.view.SwUpdate;
});
}

Common patterns

Conditional rendering based on view state

<!-- Show the place UI only when in PLACE view -->
<app-place-shell *ngIf="view.View === 'PLACE'">
<!-- Show edit tools only in EDIT mode -->
<app-edit-toolbar *ngIf="view.Mode === 'EDIT'"></app-edit-toolbar>
</app-place-shell>

<!-- Show offline banner -->
<div class="offline-banner" *ngIf="!view.IsOnline">
You are offline. Changes will sync when connectivity is restored.
</div>

Persisted configuration via viewService

The Kiosk and IsPOS flags are persisted to configService (which uses localStorage). This means they survive page reloads without any extra code. When you set this.view.Kiosk = true, the value is written to localStorage under upp_cfg_VIEW and also pushed to platformService._isKiosk, which emits OnKioskChanged.

Gotchas and tips

  • Setting Access also changes View: if you set this.view.Access = 'GUEST', the view automatically switches to 'PLACE'. This is intentional -- guests go straight to the place view.
  • OnViewChanged fires frequently: it fires on view changes, mode changes, language loads, and SW updates. Keep your subscriber handlers lightweight.
  • Guest theme is locked to dark: setting Theme on a guest session is a no-op. The setter checks Access === 'GUEST' and returns early.
  • GrantViewChange is async: you must await it before navigating. If any subscriber calls OnCloseResponse(false), the navigation is blocked.
  • Each EditMode has its own MainTab: when you switch from VIEW to EDIT and back, the tab selection is restored. This is stored in an in-memory Map, not persisted.
ServiceRelationship
stateServiceviewService.Access delegates to stateService.Access. Session expiry resets the view.
platformServiceTheme, kiosk, mobile/desktop detection, zoom, scrollbar.
httpServiceOnline/offline status.
configServicePersists VIEW configuration (kiosk, ispos).
languageServiceLanguage module loading triggers OnViewChanged.