vps_utilities

A multiplatform utilities package for VPS Citizen Mobile application. It runs on all Flutter-supported platforms from a single codebase.

Supported platforms: iOS, Android, Web, Windows, macOS, Linux.

Author: Levis Nyingi

License: Private / proprietary. This software is for use only in ISE Intelligent Security Ecosystem software. See LICENSE.

Provides a network layer (HTTP client with Dio), base URL config, secure token storage, flexible secure key-value storage (drop-in replacement for an app-level secure storage data source), Cubit state management for tokens, registry (shared models and data source for officer/citizen registry, stations, and officers), form engine (configurable forms with paging, validation, optional AI narrative, and context.pushForm), cache, device info, and responsive layout plus device type detection for platform-aware UI, using Clean Architecture and Kiwi for dependency injection. Storage uses flutter_secure_storage (encrypted where supported, persists across restarts). No Material UI—works on mobile, desktop, and web.

Full documentation

Complete documentation for every area of the package is in the docs folder:

Topic Doc
Initialization, core, DI docs/initialization.md
Network layer, API errors, guardedApiCall docs/network.md
Token API, device token, TokenCubit docs/token.md
Secure storage (flexible key-value) docs/secure_storage.md
Cache (CacheManager) docs/cache.md
Device info (DeviceInfoProvider) docs/device_info.md
Responsive layout, device type docs/responsive.md
Registry (shared models, RegistryDataSource) docs/registry.md
Form engine (VpsFormEngine, pushForm) docs/form_engine.md
Narrative (AI narrative service) docs/narrative.md

docs/README.md – Index of all documentation.

Installation

From a local path (same repo)

Add this package to your pubspec.yaml:

dependencies:
  vps_utilities:
    path: flutter packages/vps_utilities

From GitHub

If the package is stored in GitHub, depend on it via git:

dependencies:
  vps_utilities:
    git:
      url: https://github.com/YOUR_ORG_OR_USER/vps_utilities.git
      ref: main   # or master, or a tag e.g. ref: v1.1.0

For a private repo, use SSH or a personal access token in the URL. Then run:

flutter pub get

Platform notes

The package is written in Dart/Flutter and does not depend on platform-specific APIs directly. Secure token storage uses flutter_secure_storage, which has platform-specific implementations (e.g. Keychain on iOS, Keystore on Android, in-memory or alternatives on Web). The same API works on all platforms; behavior and backing store may differ per platform.

Initialization

Important: You must initialize the package with a base URL before using any functionality.

Initialize the package in your main() function before running your app:

import 'package:flutter/material.dart';
import 'package:vps_utilities/vps_utilities.dart';

void main() {
  VpsUtilities.initialize(
    baseUrl: 'https://api.example.com/api/v1/',
  );

  runApp(MyApp());
}

Network layer

Resolve and use the HTTP client via Kiwi:

final networkService = VpsUtilities.resolve<NetworkService>();

// GET
final response = await networkService.getHttp('/users', tokenRequired: true);

// POST
final response = await networkService.postHttp(
  '/login',
  body: {'email': email, 'password': password},
  tokenRequired: false,
);

Methods: getHttp, postHttp, putHttp, deleteHttp. Each accepts endpoint, optional params, headers, body (where applicable), tokenRequired, and optional per-request token.

Response shape: On success the map has data. On failure it has error (an ApiErrors value) and message. Use response['error'] as ApiErrors for handling (e.g. 401 → unauthenticated, 404 → notFound).

Token API

Set

Store the auth token securely (e.g. after login). Pass null to clear (e.g. on logout). Stored with flutter_secure_storage (encrypted, persists across restarts).

await VpsUtilities.setAuthToken(response['data']['token']);

// Clear on logout
await VpsUtilities.setAuthToken(null);

Device token (Device-Token header)

In addition to the auth token, the package supports a device registration token that is sent as a Device-Token HTTP header on all requests when present. This is intended for device onboarding flows shared across apps (e.g. Officer, Citizen, others).

Set / clear device token

// After successful device registration
await VpsUtilities.setDeviceToken(deviceTokenFromBackend);

// Clear when needed (e.g. logout, reset)
await VpsUtilities.setDeviceToken(null);

Read device token (optional)

final deviceToken = await VpsUtilities.getDeviceToken();

How it is used in requests

The internal RestClient reads the stored device token from secure storage and, if non-empty, adds:

  • Device-Token: <device_registration_token>

to every outgoing request. If no token is stored, the header is omitted and behavior is unchanged.

Use

When tokenRequired: true, the package adds the Authorization: Bearer <token> header automatically. It uses a per-request token if you pass one; otherwise it uses the stored token.

Per-request token

Each network method accepts an optional token parameter. If provided, that token is used for that request only (even if no token was set globally). Use this for one-off or login requests.

final networkService = VpsUtilities.resolve<NetworkService>();

// One-off request with a specific token
await networkService.getHttp(
  '/profile',
  tokenRequired: true,
  token: someToken,
);

See

Read the stored auth token when needed. Sensitive: use only when necessary (e.g. debug or passing to another layer). Do not log or display in production.

final token = await VpsUtilities.getAuthToken();

Secure storage (flexible key-value)

The package provides flexible secure storage for any key-value data (tokens, user data, etc.). It uses the same encrypted backend as the auth token (Android EncryptedSharedPreferences, iOS Keychain, etc.) and can replace an app-level secure storage data source.

Requirements: Call VpsUtilities.initialize(baseUrl: ...) first.

API (static)

Method Description
VpsUtilities.setSecureData(key, value) Store value under key. Pass null or empty to remove the key.
VpsUtilities.getSecureData(key) Returns Future<String?> — the value for key, or null if not found.
VpsUtilities.removeSecureData(key) Removes the value for key.
VpsUtilities.clearAllSecureData() Removes all keys. Use with care (e.g. on logout).
VpsUtilities.containsSecureKey(key) Returns Future<bool> — whether key exists.

Examples

// Store sensitive data
await VpsUtilities.setSecureData('user', jsonEncode(userJson));

// Read
final userJson = await VpsUtilities.getSecureData('user');

// Check existence
final hasUser = await VpsUtilities.containsSecureKey('user');

// Remove one key
await VpsUtilities.removeSecureData('user');

// Clear all (e.g. logout)
await VpsUtilities.clearAllSecureData();

Resolving SecureStorage

For dependency injection or multiple operations, resolve the SecureStorage instance:

final storage = VpsUtilities.resolve<SecureStorage>();

await storage.write('key', 'value');
final value = await storage.read('key');
await storage.delete('key');
await storage.deleteAll();
final exists = await storage.containsKey('key');

Replacing an app-level secure storage

If your app has a SecureStorageDataSource (or similar) that uses flutter_secure_storage with storeSecureData / getSecureData / removeSecureData / clearAllSecureData / containsKey, you can route those calls to VpsUtilities.setSecureData, VpsUtilities.getSecureData, etc., after initializing the package. The auth token is already handled via Token API; use the flexible API for other sensitive keys (e.g. user profile, IPRS data).

Token usage examples

After login:

await VpsUtilities.setAuthToken(response['data']['token']);

One-off request with a different token:

final networkService = VpsUtilities.resolve<NetworkService>();
await networkService.getHttp(
  url,
  tokenRequired: true,
  token: oneOffToken,
);

Accessing the base URL:

String baseUrl = VpsUtilities.baseUrl;

// Or via domain contract
BaseUrlConfigRepository config = VpsUtilities.resolve<BaseUrlConfigRepository>();
String baseUrl = config.baseUrl;

State management (Cubit)

The package provides TokenCubit for token state. It syncs with the secure token store and emits TokenState (e.g. hasToken, optional token). Use BlocProvider and BlocBuilder / BlocListener in your app.

Provide the cubit:

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:vps_utilities/vps_utilities.dart';

// After VpsUtilities.initialize(...)
BlocProvider<TokenCubit>(
  create: (_) => VpsUtilities.resolve<TokenCubit>(),
  child: MyApp(),
)

React to token state:

BlocBuilder<TokenCubit, TokenState>(
  builder: (context, state) {
    if (state.hasToken) {
      return HomeScreen();
    }
    return LoginScreen();
  },
)

Update token (login / logout):

// After login
context.read<TokenCubit>().setToken(response['data']['token']);

// Logout
await context.read<TokenCubit>().clearToken();

Calling VpsUtilities.setAuthToken also updates TokenCubit state so the UI stays in sync.

Responsive layout and device type

The package provides Tailwind-style breakpoints, device/platform detection, and layout helpers so you can build responsive, platform-aware UI in one place.

Device type (no BuildContext)

Use VpsResponsive for quick checks. It uses the registered DeviceTypeProvider when VpsUtilities is initialized; otherwise it falls back to DeviceTypeProviderImpl, so device type works with or without DI.

import 'package:vps_utilities/vps_utilities.dart';

// Quick checks (no initialization required for fallback)
if (VpsResponsive.isWeb) { /* web-only UI */ }
if (VpsResponsive.isAndroid) { /* Android-specific */ }
if (VpsResponsive.isIOS) { /* iOS-specific */ }
if (VpsResponsive.isMobileOS) { /* Android or iOS */ }

// Full enum (e.g. for switch or logging)
DeviceType type = VpsResponsive.deviceType;

Resolve via DI (after VpsUtilities.initialize):

final provider = VpsUtilities.resolve<DeviceTypeProvider>();
DeviceType type = provider.current;

Responsive layout (with BuildContext)

Use ResponsiveLayout with MediaQuery and Tailwind breakpoints (sm 640, md 768, lg 1024, xl 1280, 2xl 1536). All methods take BuildContext.

Breakpoints (mobile-first, “at least”):

if (ResponsiveLayout.isSm(context)) { /* width >= 640 */ }
if (ResponsiveLayout.isMd(context)) { /* width >= 768 */ }
if (ResponsiveLayout.isLg(context)) { /* width >= 1024 */ }
if (ResponsiveLayout.isXl(context)) { /* width >= 1280 */ }
if (ResponsiveLayout.is2xl(context)) { /* width >= 1536 */ }

Orientation:

if (ResponsiveLayout.isPortrait(context)) { /* portrait */ }
if (ResponsiveLayout.isLandscape(context)) { /* landscape */ }

Layout helpers:

// Responsive padding (16 below md, 24 at md–lg, 32 at lg+)
padding: ResponsiveLayout.getResponsivePadding(context),

// Max content width for centered layouts (infinity / 900 / 1200 by breakpoint)
maxWidth: ResponsiveLayout.getMaxContentWidth(context),

// Center content with max width when at least md
ResponsiveLayout.centerContent(
  context: context,
  child: myWidget,
  maxWidth: 1200, // optional; defaults to getMaxContentWidth(context)
)

Breakpoint constants (for custom logic): TailwindBreakpoints.sm, TailwindBreakpoints.md, TailwindBreakpoints.lg, TailwindBreakpoints.xl, TailwindBreakpoints.xxl.

Pure width-based checks (no BuildContext): use BreakpointResolver with a width value (e.g. from MediaQuery.sizeOf.width):

double width = MediaQuery.sizeOf(context).width;
if (BreakpointResolver.isMd(width)) { /* width >= 768 */ }

When to use what

Need Use
“Am I on web / Android / iOS?” VpsResponsive.isWeb, VpsResponsive.isAndroid, VpsResponsive.isIOS, or VpsResponsive.deviceType
“Is viewport at least sm/md/lg?” ResponsiveLayout.isSm/isMd/isLg
“Portrait or landscape?” ResponsiveLayout.isPortrait/isLandscape
“Padding / max width by breakpoint?” ResponsiveLayout.getResponsivePadding, ResponsiveLayout.getMaxContentWidth
“Center content with max width?” ResponsiveLayout.centerContent
“Width >= breakpoint without context?” BreakpointResolver.isSm/isMd/…(width)

Registry

The package provides a shared registry utility for VPS Officer and VPS Citizen apps: shared models (registry entries, stations, officers) and a RegistryDataSource that uses the package NetworkService and guardedApiCall to fetch registry data. No app-specific network layer is required; the registry lives in the package and reuses the package network. The form engine uses SubModule and Field from the registry. See Registry for full documentation.

Purpose

  • Officer app: Fetch registry list (microservices/modules) via a single endpoint (e.g. officer_registry), typically without auth.
  • Citizen app: Fetch registry plus police stations and officers in one response (endpoint e.g. registry), with auth; optional fields such as initial_iprs and documents are supported in the shared models.

Models use optional fields so one canonical type works for both apps: e.g. RegistryModel has optional initial_iprs and documents (citizen uses them; officer receives null when the API omits them). RegistryDatum has optional initial_iprs. StationResponseModel and StationOfficer are used when the citizen app’s registry response includes stations and officers.

Resolving and using RegistryDataSource

After Initialization, resolve the data source and call the appropriate method:

final registryDataSource = VpsUtilities.resolve<RegistryDataSource>();

// Officer-style: registry list only, no token
final registryList = await registryDataSource.getRegistry(tokenRequired: false);

// Citizen-style: registry + stations + officers in one call (with token)
final result = await registryDataSource.getRegistryWithStationsAndOfficers(
  tokenRequired: true,
);
// result.data   -> List<RegistryModel>
// result.stations -> List<StationResponseModel>? (may be null if API omits)
// result.officers -> List<StationOfficer>? (may be null if API omits)

On API or network failure, both methods throw GuardedApiException; apps can catch it and map to their own domain errors or UI messages.

Endpoint and token

The default registration in the package uses endpoint registry and defaultTokenRequired: false. For the officer app (endpoint officer_registry, no token), register your own RegistryDataSourceImpl in app DI after setup, e.g.:

// In app DI, after VpsUtilitiesInjector.setup(baseUrl):
container.registerFactory<RegistryDataSource>(
  (c) => RegistryDataSourceImpl(
    c.resolve<NetworkService>(),
    registryEndpoint: 'officer_registry',
    defaultTokenRequired: false,
  ),
);

Alternatively, the package may be extended later to accept optional registryEndpoint and registryTokenRequired from VpsUtilities.initialize.

Exported types

Full documentation: Registry.

Form engine

The package provides a configurable form engine with paging, validation, optional AI narrative enhancement (Q&A and review dialogs), and a single-call API so the app can open a form with one line.

Quick start

  1. Initialize with base URL and optional AI narrative URL (see Initialization).
  2. Register the form engine config after initialization:
VpsUtilities.formEngineConfig = VpsFormEngineConfig(
  pageConfig: MyFormPageConfig(),   // implements VpsFormPageConfig
  theme: VpsFormTheme(...),         // optional
  fieldBuilderRegistry: myRegistry, // optional; custom field builders
);
  1. Open a form from anywhere with a BuildContext. For per-module base URLs (e.g. from registry), set your page config’s module URL before calling pushForm (e.g. pageConfig.moduleBaseUrl = registryModel.url). See Form engine.
context.pushForm(subModule);  // subModule is the package's SubModule (from registry)

When the user taps Submit on the last page, the engine validates, runs the AI narrative flow (if enabled), then optionally runs a pre-confirm flow (e.g. occurrence location and police station selection) when your page config returns true for shouldRunPreConfirmFlow, then pushes the default confirm page (or your custom buildConfirmPage). The default confirm page matches vps_citizen: step indicator (dynamic steps; optional show/hide and numbered/unnumbered), details card with module name and field rows, Edit and Submit buttons. When the user taps Submit on the confirm page, the engine submits to sub_module_data and pushes the default success or fail page. See Form engine for pre-confirm flow (OB flow) details.

Main types

  • VpsFormPageConfig – App implements this for baseUrl, optional mapsApiKey (for location fields), and optional custom narrative/success/fail dialogs. For per-module base URLs (e.g. from registry), add a settable moduleBaseUrl and have baseUrl return moduleBaseUrl ?? VpsUtilities.baseUrl; set moduleBaseUrl before each pushForm.
  • VpsFormEngineConfig – Bundles pageConfig, optional theme, optional fieldBuilderRegistry, optional getAuthToken, and optional step indicator overrides: initialStep, showStepIndicator, showStepNumbers. See Form engine.
  • FieldBuilderRegistry – Register custom builders by field type (e.g. location, mugshot) or by field name.
  • VpsFormEngine – Root form widget (Scaffold, AppBar, step indicator, form content).
  • context.pushForm(SubModule) – Extension to open the form using the registered config.

Full documentation: Form engine.

Architecture

The package follows Clean Architecture and is organized by utility so multiple utilities are easy to add and maintain:

lib/src/
  core/                    # Shared: config + DI (used by other utilities)
    domain/                # BaseUrlConfigRepository
    data/                  # BaseUrlConfigRepositoryImpl
    di/                    # VpsUtilitiesInjector (Kiwi container)
  network/                 # Network utility
    domain/                # NetworkService, ApiErrors, guardedApiCall
    data/                  # NetworkServiceImpl, RestClient, ErrorSanitizer
  registry/                # Registry utility (uses network)
    domain/                # RegistryDataSource, RegistryResult, models
    data/                  # RegistryDataSourceImpl
  form/                    # Form engine (uses core, narrative)
    domain/                # Ports, NarrativeKeysHelper
    data/                  # FormDependencyResolverImpl, FormValidationServiceImpl
    presentation/          # VpsFormCubit, VpsFormEngine, config, field builders, pushForm
  narrative/               # AI narrative enhancement (uses core)
    domain/                # NarrativeServicePort
    data/                  # NarrativeServiceImpl, NoOpNarrativeService
  secure_storage/          # Flexible secure key-value storage
    secure_storage_impl.dart  # SecureStorage (flutter_secure_storage)
  token/                   # Token utility (uses SecureStorage)
    data/                  # TokenStore
    presentation/cubit/    # TokenCubit, TokenState
  responsive/              # Responsive layout and device type
    domain/                # DeviceType, TailwindBreakpoints, DeviceTypeProvider
    data/                  # DeviceTypeProviderImpl, BreakpointResolver
    presentation/          # ResponsiveLayout, VpsResponsive
  vps_utilities_init.dart  # Package init and public API

To add a new utility (e.g. analytics, logging): add a folder under src/ with domain/, data/, and optional presentation/, then register it in core/di/vps_utilities_injector.dart.

Dependencies are resolved via Kiwi. After initialization, use VpsUtilities.resolve<T>() to obtain registered types (e.g. NetworkService, BaseUrlConfigRepository, TokenCubit).

Regenerating Kiwi code

If you add or change @Register in the injector, regenerate the .g.dart file:

cd packages/vps_utilities
dart run build_runner build --delete-conflicting-outputs

Libraries

vps_utilities
A multiplatform utilities package for VPS Citizen Mobile application. Supports iOS, Android, Web, Windows, macOS, and Linux.