State Management
Application state is managed through two complementary services in libs/upp-base/src/modules/: stateService for session and identity data, and viewService for UI state. Both are singleton services (providedIn: 'root').
stateService
Located in libs/upp-base/src/modules/state.ts. Manages the core identity and session state.
UserMode
type UserMode = 'GUEST' | 'LOGIN';
| Mode | Description |
|---|---|
GUEST | Anonymous access via QR code scan. Limited to a single place. |
LOGIN | Authenticated access via username/password. Can manage multiple places. |
UserSession
type UserSession = {
session: string | null;
device: string | null;
};
Properties and Observables
| Property | Type | Observable | Description |
|---|---|---|---|
Access | UserMode | null | — | Current access mode (GUEST or LOGIN) |
session | string | null | OnSession | Session ID. URL-encoded. Emits when changed. |
device | string | null | OnDevice | Device UUID. URL-encoded. Emits when changed. |
place | string | null | — | Current place objid (for URL params in API calls) |
IsExpired | boolean | OnExpired | Session expiration flag. Setting to true emits. |
IsReady | boolean | OnReady | System ready flag. Setting to true emits. |
Session Lifecycle
stateDiagram-v2
[*] --> NoSession: App starts
NoSession --> Identified: device ID resolved
Identified --> Active: session assigned (login/QR)
Active --> WithPlace: place selected
WithPlace --> Active: place cleared
Active --> Expired: server returns expired
Expired --> NoSession: user acknowledges
Active --> NoSession: logout / stop
- App starts:
stateServiceis created with all valuesnull. - Device identification:
syncServiceresolves the device ID viaidentificationService.DeviceId()and setsstate.device. - Session created: After login or QR scan,
syncService.Start(session)setsstate.session. - Place selected: When a user picks a place,
state.placeis set so API calls include the place parameter. - Expiration: Server returns error code 2/3 →
state.IsExpired = true→OnExpiredemits → UI reacts. - Logout:
syncService.Stop()clearsstate.session.
viewService
Located in libs/upp-base/src/modules/view.ts. Manages all UI-related state.
ViewMode
type ViewMode = 'LOGIN' | 'USER' | 'PLACE' | 'GUEST';
| Mode | Screen | When |
|---|---|---|
LOGIN | Login form | Initial state for LOGIN access |
USER | User dashboard (place selection) | After successful login |
PLACE | Place management / POS | After place selection, or GUEST access |
GUEST | Guest (QR) view | Alias — sets View to PLACE with GUEST access |
EditMode
type EditMode = 'VIEW' | 'EDIT' | 'CART';
| Mode | Purpose |
|---|---|
VIEW | Viewing place activity (tickets, orders) |
EDIT | Configuring place parameters (catalog, settings) |
CART | Creating a new ticket (adding products) |
Properties and Observables
| Property | Type | Observable | Description |
|---|---|---|---|
View | ViewMode | null | OnViewChanged | Current screen. Setting emits a change event. |
Access | UserMode | null | — | Delegates to stateService.Access. Setting also updates View. |
Mode | EditMode | null | OnViewChanged | Current edit mode within the place screen. |
Theme | 'dark' | 'light' | OnTheme | Color theme. Guests are locked to dark. |
Mobile | boolean | — | True if device is mobile. |
Desktop | boolean | — | True if device is desktop. |
Legacy | boolean | — | True if device uses a legacy browser. |
Kiosk | boolean | OnKiosk | Kiosk mode (virtual keyboard). Persisted in local config. |
IsPOS | boolean | — | POS-specific behavior toggle. Persisted in local config. |
MainTab | string | null | OnTabChanged | Primary tab selection (per edit mode). |
ScndTab | string | null | OnTabChanged | Secondary tab selection. |
PanelArg | any | null | OnTabChanged | Arguments for the secondary panel. |
DefaultPanel | string | null | OnRightChanged | Right panel visibility/content. |
Panel | PlacePanel | null | — | Current panel configuration. |
IsOnline | boolean | OnOnline | Network connectivity status. |
SwUpdate | boolean | OnViewChanged | Service worker update available. |
CanZoom | boolean | — | Whether zoom is safe (disabled on iOS Safari). |
Access / View Relationship
Setting Access automatically updates View:
set Access(value: UserMode) {
this.state.Access = value;
switch(value) {
case 'GUEST': this.View = 'PLACE'; break;
case 'LOGIN': this.View = 'LOGIN'; break;
}
}
This ensures the correct initial screen is shown for each access mode.
Panel Close Protocol
The viewService implements a cooperative close protocol for panels that may have unsaved changes:
sequenceDiagram
participant Caller
participant viewService
participant Panel1
participant Panel2
Caller->>viewService: GrantViewChange()
viewService->>Panel1: OnCloseRequest
viewService->>Panel2: OnCloseRequest
Panel1->>viewService: OnCloseResponse(true)
Panel2->>viewService: OnCloseResponse(true)
viewService-->>Caller: Promise resolves true
- Before changing views, call
GrantViewChange()which returns aPromise<boolean>. - It emits
OnCloseRequestto all subscribers. - Each subscriber calls
OnCloseResponse(allow). - The promise resolves to
trueonly if all subscribers allow the change.
Subscribers are tracked via a counter, and the unsubscribe method is wrapped to decrement it automatically.
Tab Management
Tabs are stored per EditMode, so switching between VIEW/EDIT/CART preserves the selected tab in each mode:
private _maintab = new Map<EditMode, string | null>();
get MainTab(): string | null {
if (!this.Mode) return null;
return this._maintab.get(this.Mode) || null;
}
SetMainTab(mode: EditMode, value: string | null) {
this._maintab.set(mode, value);
if (mode == this.Mode) {
this._onTabChanged.next(value);
}
}
Configuration Persistence
View settings like Kiosk and IsPOS are persisted through configService:
get Kiosk(): boolean {
return this.configuration.get(UppViewConfiguration.kiosk) as boolean
|| this.platform._isKiosk;
}
set Kiosk(value: boolean) {
if (this.platform._isKiosk != value) {
this.configuration.set(UppViewConfiguration.kiosk, value);
this.platform._isKiosk = value;
}
}
This ensures settings survive page reloads.
State Flow in the Application
graph TD
START([App Start]) --> DEVICE[identificationService<br/>resolves device UUID]
DEVICE --> STATE_DEV[stateService.device = uuid]
STATE_DEV --> READY[syncService.IsReady emits]
READY --> QR{QR scan?}
QR -->|Yes| GUEST[viewService.Access = GUEST]
GUEST --> PLACE_VIEW[View = PLACE]
QR -->|No| LOGIN[viewService.Access = LOGIN]
LOGIN --> LOGIN_VIEW[View = LOGIN]
LOGIN_VIEW --> AUTH[User authenticates]
AUTH --> SESSION[syncService.Start<br/>stateService.session set]
SESSION --> USER_VIEW[View = USER]
USER_VIEW --> SELECT[User selects place]
SELECT --> PLACE_SET[stateService.place<br/>syncService.SetPlace]
PLACE_SET --> PLACE_VIEW
PLACE_VIEW --> MODE{Edit mode}
MODE --> VIEW_MODE[Mode = VIEW]
MODE --> EDIT_MODE[Mode = EDIT]
MODE --> CART_MODE[Mode = CART]
Device Identification
The identificationService in libs/upp-base/src/modules/device.ts generates or retrieves a unique device UUID. This UUID is:
- Persisted in local storage
- Sent with every API request via
stateService.device - Used by the server to associate connections and ticket numbering with a specific device
The syncService constructor waits for device identification to complete before emitting IsReady:
constructor(...) {
this.deviceid.DeviceId().then((deviceid) => {
this.state.device = deviceid;
this._isReady.next(true);
this._isReady.complete();
});
}