Skip to main content

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';
ModeDescription
GUESTAnonymous access via QR code scan. Limited to a single place.
LOGINAuthenticated access via username/password. Can manage multiple places.

UserSession

type UserSession = {
session: string | null;
device: string | null;
};

Properties and Observables

PropertyTypeObservableDescription
AccessUserMode | nullCurrent access mode (GUEST or LOGIN)
sessionstring | nullOnSessionSession ID. URL-encoded. Emits when changed.
devicestring | nullOnDeviceDevice UUID. URL-encoded. Emits when changed.
placestring | nullCurrent place objid (for URL params in API calls)
IsExpiredbooleanOnExpiredSession expiration flag. Setting to true emits.
IsReadybooleanOnReadySystem 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
  1. App starts: stateService is created with all values null.
  2. Device identification: syncService resolves the device ID via identificationService.DeviceId() and sets state.device.
  3. Session created: After login or QR scan, syncService.Start(session) sets state.session.
  4. Place selected: When a user picks a place, state.place is set so API calls include the place parameter.
  5. Expiration: Server returns error code 2/3 → state.IsExpired = trueOnExpired emits → UI reacts.
  6. Logout: syncService.Stop() clears state.session.

viewService

Located in libs/upp-base/src/modules/view.ts. Manages all UI-related state.

ViewMode

type ViewMode = 'LOGIN' | 'USER' | 'PLACE' | 'GUEST';
ModeScreenWhen
LOGINLogin formInitial state for LOGIN access
USERUser dashboard (place selection)After successful login
PLACEPlace management / POSAfter place selection, or GUEST access
GUESTGuest (QR) viewAlias — sets View to PLACE with GUEST access

EditMode

type EditMode = 'VIEW' | 'EDIT' | 'CART';
ModePurpose
VIEWViewing place activity (tickets, orders)
EDITConfiguring place parameters (catalog, settings)
CARTCreating a new ticket (adding products)

Properties and Observables

PropertyTypeObservableDescription
ViewViewMode | nullOnViewChangedCurrent screen. Setting emits a change event.
AccessUserMode | nullDelegates to stateService.Access. Setting also updates View.
ModeEditMode | nullOnViewChangedCurrent edit mode within the place screen.
Theme'dark' | 'light'OnThemeColor theme. Guests are locked to dark.
MobilebooleanTrue if device is mobile.
DesktopbooleanTrue if device is desktop.
LegacybooleanTrue if device uses a legacy browser.
KioskbooleanOnKioskKiosk mode (virtual keyboard). Persisted in local config.
IsPOSbooleanPOS-specific behavior toggle. Persisted in local config.
MainTabstring | nullOnTabChangedPrimary tab selection (per edit mode).
ScndTabstring | nullOnTabChangedSecondary tab selection.
PanelArgany | nullOnTabChangedArguments for the secondary panel.
DefaultPanelstring | nullOnRightChangedRight panel visibility/content.
PanelPlacePanel | nullCurrent panel configuration.
IsOnlinebooleanOnOnlineNetwork connectivity status.
SwUpdatebooleanOnViewChangedService worker update available.
CanZoombooleanWhether 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
  1. Before changing views, call GrantViewChange() which returns a Promise<boolean>.
  2. It emits OnCloseRequest to all subscribers.
  3. Each subscriber calls OnCloseResponse(allow).
  4. The promise resolves to true only 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();
});
}