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, andplaceafter authentication. - Session expiry handling: subscribe to
OnExpiredto redirect to the login screen. - System readiness: set
IsReady = truewhen all initial data is loaded; other services (likepreloadService) wait for this signal. - Backend requests:
adhocServicereadsdevice,session,placefromstateServiceon 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
| Property | Type | Description |
|---|---|---|
Access | UserMode | null | Current access mode. 'GUEST' for QR-scanned access, 'LOGIN' for authenticated users. null before any access mode is set. |
userSession | UserSession | null | Raw session object containing both session and device identifiers. |
session | string | null | URL-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. |
device | string | null | URL-encoded device identifier. Setting a new value emits OnDevice. Generated by identificationService.DeviceId() during the bootstrap flow. |
place | string | null | URL-encoded place objid. Identifies the active establishment. Used by adhocService to scope all API calls to the correct place. |
IsExpired | boolean | Session expiration flag. When the backend returns a session-expired error, the application sets this to true, which emits OnExpired and triggers the logout flow. |
IsReady | boolean | System 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
| Observable | Emits when | Typical subscriber |
|---|---|---|
OnDevice | The device ID changes. | Login flow (to confirm device registration). |
OnSession | The session ID changes. | Components that need to react to login/logout. |
OnExpired | The session expires (IsExpired set to true). | viewService resets the view; app navigates to login. |
OnReady | The 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
Viewto switch between LOGIN, USER, PLACE, and GUEST screens. - Mode switching: change
Modeto switch between VIEW (read-only), EDIT (configuration), and CART (ticket creation). - Panel management: use
DefaultPanel,MainTab,ScndTab, andPanelArgto control the right-side panel and tab navigation. - Theme toggling: set
Themeto 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
IsOnlineandOnOnline.
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
| Property | Type | Description |
|---|---|---|
View | ViewMode | null | Current screen. Setting a new value emits OnViewChanged. Set to null to indicate no active view (e.g. after session expiry). |
Access | UserMode | null | Delegates 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. |
Mode | EditMode | null | Current edit mode within the PLACE view (VIEW, EDIT, or CART). Setting a new value emits OnViewChanged. |
Device and display
| Property | Type | Description |
|---|---|---|
CanZoom | boolean | false on iOS Safari (stability issues), true otherwise. Use this to conditionally show zoom controls. |
Theme | 'dark' | 'light' | null | Current theme. Setting it delegates to platformService._theme, which adds/removes the CSS class on document.body. Guests are locked to 'dark'. |
LightTheme / DarkTheme | boolean | Convenience getters for template bindings. |
Scrollbar | boolean | Scrollbar visibility. Delegates to platformService._scrollbar. |
Mobile / Desktop | boolean | Device category. Delegates to platformService._isMobile / _isDesktop. |
Legacy | boolean | true for legacy mobile (phones) or legacy desktop devices. Used to adapt layouts for devices with native keyboards vs. touch-only devices. |
Kiosk | boolean | Virtual-keyboard kiosk mode. When true, the <upp-kiosk-board> component shows an on-screen keyboard. Persisted via configService so the setting survives page reloads. |
IsPOS | boolean | POS-specific behaviour flag. Persisted via configService. When true, enables Point of Sale optimisations. |
Panels and tabs
| Property / Method | Type | Description |
|---|---|---|
DefaultPanel | string | null | The default right-side panel identifier. Emits OnRightChanged when changed. |
MainTab | string | null | Primary tab for the current EditMode. Each mode has its own stored tab, so switching modes restores the last-active tab. |
SetMainTab(mode, value) | void | Sets the primary tab for a specific mode. Emits OnTabChanged if the mode is the current one. |
ScndTab | string | null | Secondary tab within the current mode. |
PanelArg | any | null | Arguments for the secondary panel. Setting this emits OnTabChanged. |
Panel | PlacePanel | null | Current 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.
| Member | Description |
|---|---|
OnCloseRequest | Custom 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
| Property | Type | Description |
|---|---|---|
IsOnline | boolean | Delegates to httpService.IsOnline. Prefer using this over injecting httpService directly in components. |
OnOnline | Observable<boolean> | Delegates to httpService.OnOnline. |
SwUpdate | boolean | Set to true when a new service-worker version is detected. Emits OnViewChanged so the UI can show an update banner. |
Observables
| Observable | Emits when | Typical use |
|---|---|---|
OnViewChanged | View, mode, SW update, or language changes. | Main refresh trigger for components. |
OnRightChanged | The default right panel changes. | Right-panel container listens to show/hide panels. |
OnTabChanged | A tab changes (main or secondary). | Tab bar components refresh their selection state. |
OnTheme | The theme changes (delegates to platformService). | Components that need to adjust colours or icons. |
OnKiosk | Kiosk 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
Accessalso changesView: if you setthis.view.Access = 'GUEST', the view automatically switches to'PLACE'. This is intentional -- guests go straight to the place view. OnViewChangedfires 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
Themeon a guest session is a no-op. The setter checksAccess === 'GUEST'and returns early. GrantViewChangeis async: you mustawaitit before navigating. If any subscriber callsOnCloseResponse(false), the navigation is blocked.- Each
EditModehas its ownMainTab: when you switch fromVIEWtoEDITand back, the tab selection is restored. This is stored in an in-memoryMap, not persisted.
Related services
| Service | Relationship |
|---|---|
stateService | viewService.Access delegates to stateService.Access. Session expiry resets the view. |
platformService | Theme, kiosk, mobile/desktop detection, zoom, scrollbar. |
httpService | Online/offline status. |
configService | Persists VIEW configuration (kiosk, ispos). |
languageService | Language module loading triggers OnViewChanged. |