hosteday_flutter

Flutter SDK for Hosteday REST, authentication, public URLs, and optional Realtime.

Installation

This archive contains the unreleased 2.2.0 source. Until you publish it, use a local path: /path/to/hosteday-flutter dependency (as the bundled example does). After publication:

dependencies:
  hosteday_flutter: ^2.2.0
import 'package:hosteday_flutter/hosteday_flutter.dart';

Initialize Hosteday

Only the project domain is required. No network request is made by the default initialization.

await Hosteday.initializeApp(options: {
  HostedayOptionKeys.projectDomain: 'max.hosteday.com',
});

Optional values may be null. http://localhost:8787 preserves HTTP and the port. Call await Hosteday.dispose() before switching projects.

Generated app runtime config

Declare the asset in the application's pubspec.yaml:

flutter:
  assets:
    - assets/config/hosteday.json
{
  "project_domain": "max.hosteday.com",
  "project_api_key": null,
  "realtime_app_key": null,
  "realtime_host": null
}
WidgetsFlutterBinding.ensureInitialized();
await Hosteday.initializeFromAsset();

initializeFromAsset(overrides: {...}) supports explicit option overrides. The engine only needs to supply the asset and ordinary SDK resource calls.

REST API

Relative resource paths automatically receive /api. products, /products, and /api/products all target https://max.hosteday.com/api/products.

GET

final products = await Hosteday.client.get('/products');
final product = await Hosteday.client.get('/products', id: 15);

POST

final created = await Hosteday.client.post('/products',
  body: {'name': 'Phone', 'price': 250},
  withAuth: true,
);

PUT

await Hosteday.client.put('/products', id: 15,
  body: {'name': 'Phone', 'price': 240},
);

PATCH

await Hosteday.client.patch('/products/15', body: {'price': 230});

DELETE

await Hosteday.client.delete('/products', id: 15);

id is optional for PUT/PATCH/DELETE when it is already part of the path. When supplied, it is appended as one encoded segment. The high-level client's mutation methods retain withAuth: true as their default; GET and request default to false. The low-level http methods retain their default of false.

Host and URLs

final host = Hosteday.client.host;             // https://max.hosteday.com
final api = Hosteday.client.apiBaseUrl;        // https://max.hosteday.com/api

Advanced API routing:

await Hosteday.initializeApp(options: {
  HostedayOptionKeys.projectDomain: 'max.hosteday.com',
  HostedayOptionKeys.apiBaseUrl: 'https://gateway.example.com/v2',
});
// /products -> https://gateway.example.com/v2/products
// host and public links still use https://max.hosteday.com

An override containing only an origin gets /api; an override with a path uses that exact prefix. base_url remains an alias. Full REST URLs must use the configured API origin, protecting project and user credentials from accidental forwarding. Full media URLs may use other origins.

Static Pages

final info = Hosteday.client.staticPages['info'];
final privacy = Hosteday.client.staticPages['/privacy/'];
// https://max.hosteday.com/info
// https://max.hosteday.com/privacy

Dart reserves static, so the property is named staticPages. It accepts one non-empty slug and rejects traversal, query strings, fragments, and foreign URLs. It builds a link without fetching the page or checking the platform's page quota.

Storage & Media

final imageUrl = Hosteday.client.storageUrl('products/image.png');
// https://max.hosteday.com/storage/products/image.png
final cdn = Hosteday.client.storageUrl('https://cdn.example.com/image.png');
// unchanged

An existing /storage/ prefix is not duplicated. Handle absent API image values before calling storageUrl:

final imagePath = product['image'] as String?;
if (imagePath != null && imagePath.isNotEmpty) {
  Image.network(Hosteday.client.storageUrl(imagePath));
}

Authentication

final credential = await Hosteday.auth.signInWithEmailAndPassword(
  email: 'customer@example.com', password: password,
);
final orders = await Hosteday.client.get('/orders', withAuth: true);
await Hosteday.auth.signOut();
await Hosteday.auth.createUserWithEmailAndPassword(
  email: email, password: password,
  additionalData: {'name': 'Customer'},
);
await Hosteday.auth.sendPasswordResetEmail(email: email);
await Hosteday.auth.reload();
await Hosteday.auth.sendEmailVerification();

Observe Hosteday.auth.authStateChanges() and read Hosteday.auth.currentUser. For profile, avatar, password reset, session, and channel details see the API reference.

Default auth storage is in memory. To persist sessions:

await Hosteday.initializeApp(
  options: {HostedayOptionKeys.projectDomain: 'max.hosteday.com'},
  authStorage: HostedaySharedPreferencesAuthStorage(),
);

SharedPreferences is not encrypted storage. You can implement HostedayAuthStorage for your platform's secure storage. For an external user session, supply a HostedayTokenProvider; the SDK's active session takes priority.

Project API Key

await Hosteday.initializeApp(options: {
  HostedayOptionKeys.projectDomain: 'max.hosteday.com',
  HostedayOptionKeys.projectApiKey: projectApiKey,
});

The existing platform header is preserved: X-Api-Token. Null, empty, and whitespace-only keys send no header. apiTokenHeader supports an explicit server-specific override. This key is separate from Authorization: Bearer ..., which is added only for withAuth: true (or an explicitly supplied header). Client configuration is visible in a compiled application; use client-scoped keys.

final products = await Hosteday.client.get('/products',
  search: 'phone',
  queryParameters: {'category_id': 5, 'page': 2},
  filters: {'status': ['active', 'pending']},
);

Existing URL query values are preserved unless overridden. Null values are omitted. Nested maps and lists use Laravel bracket syntax. Filters apply to index GET requests only. Relation scoping remains available:

await Hosteday.client.get('/products', id: 15,
  relationField: 'category_id', relationValue: 5,
);

Pagination / Responses

Responses retain the server's JSON structure, including pagination metadata. A top-level JSON list is wrapped as {'data': list}. Empty responses return {}. No sample records or fallback products are injected.

final response = await Hosteday.client.get('/products',
  queryParameters: {'page': 2},
);
final products = response['data'];
final nextPage = response['next_page_url'];

The server determines the exact pagination envelope. The example displays the complete JSON and reads both data: [] and data: {data: []} shapes.

Error Handling

try {
  await Hosteday.client.get('/products', timeout: const Duration(seconds: 15));
} on HostedayException catch (error) {
  final status = error.statusCode;
  final developerMessage = error.message;
  final userMessage = error.displayMessage;
  final originalCause = error.error;
  final emailError = error.firstErrorFor('email');
}

Network failures and timeouts have distinct messages. Invalid JSON retains the HTTP status and original body in response['raw_body']. HTTP errors retain the server message, status, and validation errors, including 429, 430–433, and 5xx. No undocumented meaning is assigned to Hosteday-specific status codes.

For compatibility, missing/invalid configuration throws ArgumentError, and access before initialization or repeated initialization throws StateError, with specific corrective messages. Runtime asset read/JSON errors use HostedayException.

Realtime

REST works without Realtime configuration. To connect, supply both settings:

await Hosteday.initializeApp(options: {
  HostedayOptionKeys.projectDomain: 'max.hosteday.com',
  HostedayOptionKeys.realtimeAppKey: realtimeAppKey,
  HostedayOptionKeys.realtimeHost: realtimeHost,
});
if (Hosteday.config.hasRealtime) {
  await Hosteday.connectRealtime();
}

Listen using listenPublic, listenPrivate, or listenPresence as shown in the Realtime example. Private and presence subscriptions require user authentication. Publishing follows the same distinction:

await Hosteday.client.publishPublicEvent(
  channel: 'products', event: 'updated', payload: {'id': 15},
);
await Hosteday.client.publishPrivateEvent(
  channel: 'orders', event: 'updated', payload: {'id': 42},
);
await Hosteday.disconnectRealtime();

Complete Example

This minimal app displays the real server response:

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

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Hosteday.initializeApp(options: {
    HostedayOptionKeys.projectDomain: 'max.hosteday.com',
  });
  final request = Hosteday.client.get('/products');
  runApp(MaterialApp(home: Scaffold(body: SafeArea(
    child: FutureBuilder<Map<String, dynamic>>(
      future: request,
      builder: (context, snapshot) {
        if (snapshot.hasError) return Text(snapshot.error.toString());
        if (!snapshot.hasData) return const CircularProgressIndicator();
        return SingleChildScrollView(child: Text(snapshot.data.toString()));
      },
    ),
  ))));
}

The interactive example includes all HTTP verbs, search, query parameters, real response rows and images, URL helpers, account flows, and optional Realtime. On Web, /categories is sent to /api/categories; CORS must be configured by the server.

Migration and verification

Old HosteDay... spellings remain deprecated compatibility APIs. New code uses Hosteday.... See migration notes for URL behavior changes, architecture, and verification status.

flutter pub get
dart format lib test example/lib example/test
flutter analyze
flutter test
cd example
flutter pub get
flutter analyze
flutter test

This is a Flutter package: run its flutter_test suite with flutter test, not standalone dart test.

Libraries

hosteday_flutter