mirror of
https://github.com/hanskokx/arcane_framework.git
synced 2026-08-12 06:10:55 +02:00
e0f62df70a
* [UNTESTED] Fixes notifiers and adds some additional methods. Adds tests. Changes: // ArcaneEnvironment breaking: context.read<ArcaneEnvironment>() -> ArcaneEnvironment.of(context) breaking: context.read<ArcaneEnvironment>().state -> ArcaneEnvironment.of(context).environment; // Feature flag service added: reset() // Logging service added: registerInterface() added: unregisterInterfaces() added: unregisterAllInterfaces() // ArcaneReactiveTheme fixed: currentMode, dark, light now actually emit new values when changed added: getters for lightTheme, darkTheme, and systemTheme TODO: test systemTheme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * ArcaneReactiveTheme now optionally takes a ThemeMode parameter when calling switchTheme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Added `of(context)` extension to `ArcaneService` * Updated changelog * Logging service: - added: reset() - added: `skipAutodetection` option for module, method, and metadata - added: unregisterInterface() - Created FileAndLineNumber mixin Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fix module/method/fileAndLineNumber calculations in logging service Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Minor fix to fileAndLineParts calculation Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove debug logging Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Minor change Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fixed theme - fixed: currentMode, dark, light now actually emit new values when changed - added: getters for lightTheme, darkTheme, and systemTheme - breaking: currentMode -> systemTheme - added: currentTheme - breaking: currentMode => currentTheme - change: Arcane.theme.followSystemTheme(context) required to follow system theme - change: After following the system theme, calling Arcane.theme.switchTheme() cancels following the system theme - added: ArcaneReactiveTheme is now registered in ArcaneServiceProvider when using ArcaneApp Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Removed arcane theme from service provider Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fixed late initialized value in logging service * Fixes the reactive theme service to properly follow the system brightness Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fixes tests and updates reactive theme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fixes broken tests Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fixes bug with following/unfollowing system theme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Re-add getters on ArcaneTheme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Update example Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Added logStream to logger. Updated example code. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Setting a theme style now automatically switches to that theme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Updated theme example to set color of theme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Added examples for feature flags Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Added environment example Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Center text Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Added environment logging Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Rename onEnvironmentChanged to switchEnvironment Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Made ArcaneServiceProvider.of(context) nullable Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove invalid test Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Extra log messages will not longer be added to the log stream if more than one LoggingInterface is registered Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Only send log messages to the LoggingInterfaces if the logging service has been initialized Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Check for empty interfaces before initializing them in the logger Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Moved platform brightness checking from ArcaneApp to ArcaneThemeSwitcher, where it is more appropriate Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Correctly export the theme switcher widget Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove unused import Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Made the switchEnvironment field in ArcaneEnvironment private (_switchEnvironment) Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Testing GH actions Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Set correct .puro.json filename in github action config Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Check different path for .puro.json Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Test different GH action workflow Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Attempt to correctly address "stable" version in .puro.json Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Another GH action attempt Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Add environment variables Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Debug Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Add puro config to git :) Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove debug Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * feat(framework): Overhauls framework core, services, and app integration for 2.0.0 (#5) * Made ArcaneTheme private (_ArcaneTheme) Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Updated changelog Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fix updateShouldNotify on ArcaneServiceProvider Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Update service provider Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Put service locators in an extension Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Try using a mixin instead Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove 'of' and 'requiredOf' locators Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Re-add service locators. They should be called as: ArcaneService.of<MyService>(context) Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Renamed getters on ArcaneService Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Renamed serviceInstances => registeredServices and added a removeService method Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Broke up arcane service into separate files Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Added requiredServiceOfType getter on ArcaneServiceProvider Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Updated services section of the readme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Updated example to add a services example Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Removed unused variable Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Update example documentation Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Update example Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Breaking up example into smaller widgets Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove bloc dependency Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fixes the notifier for feature flags and updates the example Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fixed a bug in the example Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Update ServiceProvider to make service instances optional Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove unnecessary notifyListeners calls Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Organize theme service Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fix authentication interface and service methods for logout and login. * Fix DebugAuthInterface logout method Added environment key to ArcaneEnvironment constructor Renamed switchEnvironment parameter in ArcaneEnvironment constructor Bumped arcane_helper_utils dependency version * Enhance theme management with StreamBuilder for dynamic updates Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Add configuration files and update authentication service error handling - Introduced .vscode/settings.json and .vscode/launch.json for IDE configuration. - Updated DebugAuthInterface and ArcaneAuthenticationService to return const Result.ok() for consistency. - Added ArcaneTheme class for theme management. - Updated pubspec.yaml to change result_monad dependency version. - Modified authentication_service_test to return const Result.ok() in mock setups. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Update dependency versions to use 'any' for arcane_helper_utils and arcane_analysis Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove unnecessary 'const' keyword from Result.ok calls in DebugAuthInterface methods Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Add 'const' keyword to Result.ok calls in DebugAuthInterface methods Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Enhance logging service with new lifecycle capabilities and interceptor support - Updated `LoggingInterface` to remove singleton-style initialization. - Introduced `LoggingInitializable` and `LoggingInitializationMixin` for optional lifecycle management. - Added `LogEvent` and `LogInterceptor` classes for improved logging event handling and interception. - Updated `ArcaneLogger` to support multiple logging interfaces and global interceptors. - Modified `DebugPrint` and `DebugAuthInterface` to align with new logging structure. - Enhanced tests to cover new logging features and interceptor functionality. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Add stream-based updates for authentication, feature flags, logging, and theming - Added `statusChanges` and `signedInChanges` streams to `ArcaneAuth` for real-time authentication updates. - Introduced `enabledFeaturesChanges` stream in `ArcaneFeatureFlags` for observing feature updates. - Improved `ArcaneLogger` to maintain `logStream` usability after listener cancellation. - Updated `ArcaneTheme` to provide streams for theme mode and theme data changes. - Enhanced documentation and examples to reflect new stream APIs and usage patterns. - Added regression tests for stream listener cancellation and re-subscription across services. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Refactor ArcaneEnvironment and AuthenticationService for improved environment management and status handling Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Add application environment management and tests for authentication status coherence Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * feat: Enhance service management with ArcaneApp provider composition This commit significantly refactors Arcane's service architecture. ArcaneApp now acts as a StatefulWidget, composing InheritedWidget- based providers for core services (features, environment, theme, auth). Service lookups (e.g., Arcane.features, context.service()) now prioritize provider-registered instances, falling back to singletons. Key changes include: - Renamed ArcaneFeatureFlags to ArcaneFeatureFlagService and ArcaneReactiveTheme to ArcaneThemeService (with typedefs). - Introduced ArcaneEnvironmentService and ArcaneEnvironmentProvider for centralized environment management. - Added BuildContext extensions (e.g., context.featureFlags) for reactive, convenient service access. - Removed .puro.json and its references, streamlining SDK setup. - Updated CHANGELOG.md to reflect a 2.0.0 release, detailing all breaking changes and new features. * chore(example): Add ignore rules for example builds * feat(logging): Introduce log interceptors and origin control Introduces the LogInterceptor class, enabling pre-processing, modification, or suppression of log events before they reach registered interfaces. The LogInterceptorContext provides contextual information such as the originating LoggingInterface. Adds a `skipAutodetection` parameter to Arcane.log, allowing control over the automatic detection of log origin (module, method, file/line). Also makes the LogEvent class extendable to support custom event types and updates existing interceptor callback signatures for consistency. * fix(theme): Update theme mode check to use currentModeOf method Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * fix(theme): Improve theme mode detection and toggling `context.isDarkMode` now reflects the app's effective theme brightness, not the raw platform brightness. This ensures `isDarkMode` accurately reports the currently rendered theme. `switchTheme()` now intelligently toggles from the *effective* theme when in `ThemeMode.system` by considering the current app brightness. `followSystemTheme()` is updated to always read the platform brightness directly, decoupling its behavior from the app's overridden theme settings. * feat(example): Improve theme and service integration demo Updates the example application to better demonstrate service and theme integration. The `FavoriteColorService` now actively sets the app's color scheme and can initialize its state from the current theme. Environment display is now reactive, and the UI responds dynamically to service registration and removal. Initial themes are now explicitly seeded. * feat(example): Add interactive persistent metadata toggle Previously, persistent metadata was added unconditionally. This change allows users to dynamically enable or disable the demo persistent metadata via a UI switch, better showcasing the API functionality. * feat(theme): Add assignment-style theme setters Introduces convenience setters for dark and light themes, allowing `Arcane.theme.dark = ...` and `Arcane.theme.light = ...` as an alternative to `setDarkTheme` and `setLightTheme`. This improves developer ergonomics and conciseness. * docs(README): Add Application Environments section to features list Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Adds `ArcaneApp.builder` to provide a provider-aware `BuildContext` for the application root. This deprecates the `ArcaneApp.child` parameter, improving developer experience for accessing Arcane's providers at build time. Updates the `ArcaneThemeSwitcher` to default to `followSystemTheme` when mounted under `ArcaneApp`, ensuring system theme behavior is enabled out of the box. Fixes `setDarkTheme` and `setLightTheme` to only update the rendered theme if their respective modes are currently active. This prevents unintended UI changes when updating the definition of an inactive theme. Updates README and examples to reflect the new `builder` API and clarified theme logic. Adds new tests for these changes. * docs(arcane_app): Clarify ArcaneApp API and provider access documentation Updates the documentation for the `ArcaneApp` constructor to explicitly detail the preferred `builder` API and the deprecated `child` parameter, including migration guidance. Also updates the list of provided services to include `ArcaneFeatureFlagsProvider`. * feat: Overhaul Arcane framework for 2.0.0 release notes Migrate Arcane to a static utility surface, removing the instantiable singleton constructor for simpler and more direct access to services. Introduce new reactive streams for environment, authentication, and theme services, enabling real-time observation of state changes. Enhance `ArcaneApp` integration with the new `builder` callback and `BuildContext` convenience accessors for services and feature flags. Update `ArcaneAuthInterface.logout` signature and revise `src` import paths, requiring consumers to update their implementations and imports. Deprecate `ArcaneApp.child` and `BuildContext.serviceOfType<T>()` in favor of new, more idiomatic APIs. Remove direct `flutter_bloc` dependency and upgrade `result_monad` to `^4.0.0`. * refactor(environment): Rename environment access getter to current This change unifies how the current application environment is accessed across Arcane's services and providers. The `state` and `environment` getters have been replaced with a consistent `current` getter, simplifying API usage and aligning with new patterns. Includes corresponding updates in example and internal code, and documents the migration in the CHANGELOG. * chore: Relax result_monad dependency constraint The `result_monad` dependency was recently upgraded to `^4.0.0`. This update relaxes the constraint to `any` to provide greater flexibility and mitigate potential dependency conflicts for consumers of Arcane, allowing them to use other compatible versions. * refactor(logging): Replace LoggingInitializationMixin with LoggingInitialization and add LoggingFeature annotation support Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * docs(changelog): Add migration steps for ArcaneThemeService and update themeMode configuration examples Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * refactor(workflow): Simplify analyze-and-test workflow by removing unused environment variables and steps Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * docs(changelog): Update Arcane.log metadata type and add migration steps for structured values Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * test: Add unit tests for authentication and feature flags functionality Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * feat(theme): track user-defined theme overrides in ArcaneThemeService Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * refactor(service): update service access methods in ArcaneServiceProvider Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * refactor(tests): format login and logout method signatures in MockAuth for improved readability Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * feat(tests): add unit tests for ArcaneThemeService functionality Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> --------- Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * fix(dependencies): update result_monad and arcane_helper_utils constraints Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * fix(version): update version from 2.0.0-dev to 2.0.0 Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * feat(auth): add callbacks for login/logout events in DebugAuthInterface fix(logging): ensure proper cancellation of log stream subscriber refactor(arcane): make built-in services list unmodifiable fix(app): update service notifier on widget updates fix(service): improve removeService method to return boolean fix(auth): update BuildContext parameter to unused in setDebug/setNormal fix(logging): correct log level reference in LogInterceptorContext documentation fix(theme): update documentation to reflect ThemeMode instead of ThemeData fix(theme): ensure unawaited cancellation of theme subscriptions chore(dependencies): update arcane_analysis version to ^1.0.4 test: make ArcaneAuthenticationService reset asynchronous in tests Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * feat(theme): enhance theme synchronization and documentation for ArcaneApp Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * fix(gitignore): add example/.metadata to .gitignore Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> --------- Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> Co-authored-by: Hans Kokx <hans.kokx@airahome.com>
1092 lines
31 KiB
Markdown
1092 lines
31 KiB
Markdown
# Arcane Framework
|
||
|
||
> _**A**gnostic **R**eusable **C**omponent **A**rchitecture for **N**ew
|
||
> **E**cosystems_
|
||
|
||

|
||
|
||
The Arcane Framework is a powerful Dart package designed to provide a robust
|
||
architecture for managing key application services such as logging,
|
||
authentication, feature flags, theming, and more. This framework
|
||
is ideal for building scalable applications that require dynamic configuration
|
||
and service management.
|
||
|
||
- [Arcane Framework](#arcane-framework)
|
||
- [Features](#features)
|
||
- [Installation](#installation)
|
||
- [ArcaneApp Builder Migration (v1.x -\> v2.x)](#arcaneapp-builder-migration-v1x---v2x)
|
||
- [Arcane.log Metadata Migration (v1.x -\> v2.x)](#arcanelog-metadata-migration-v1x---v2x)
|
||
- [Usage](#usage)
|
||
- [Services](#services)
|
||
- [Defining an example `ArcaneService`](#defining-an-example-arcaneservice)
|
||
- [Registering and unregistering an `ArcaneService`](#registering-and-unregistering-an-arcaneservice)
|
||
- [Locating an `ArcaneService`](#locating-an-arcaneservice)
|
||
- [Using `ArcaneService` services](#using-arcaneservice-services)
|
||
- [Feature Flags](#feature-flags)
|
||
- [Logging](#logging)
|
||
- [Authentication](#authentication)
|
||
- [Application Environments](#application-environments)
|
||
- [Dynamic Theming](#dynamic-theming)
|
||
- [Contributing](#contributing)
|
||
|
||
## Features
|
||
|
||
- **Service Management**: Centralized access to multiple services (logging,
|
||
authentication, theming, etc.).
|
||
- **Feature Flags**: Dynamically enable or disable features using
|
||
`ArcaneFeatureFlagService`.
|
||
- **Logging**: Easily log messages with metadata, stack traces, and different
|
||
log levels via `ArcaneLogger`.
|
||
- **Authentication**: Built-in support for handling user authentication
|
||
workflows.
|
||
- **Dynamic Theming**: Switch between light and dark themes and update theme
|
||
definitions on-the-fly with `ArcaneThemeService`.
|
||
- **Extensible Service Definitions**: Implement your own `ArcaneService` services and leverage the inherent powers of Arcane.
|
||
- **Realtime Streams**: In addition to `ValueNotifier`s, core services expose
|
||
broadcast streams for reactive consumers.
|
||
|
||
## Installation
|
||
|
||
To use Arcane Framework in your Dart or Flutter project, follow these steps:
|
||
|
||
1. Add the dependency to your `pubspec.yaml`:
|
||
|
||
```shell
|
||
flutter pub add arcane_framework
|
||
```
|
||
|
||
2. (optional) Wrap your `MaterialApp` or `CupertinoApp` with `ArcaneApp`.
|
||
`ArcaneApp` wires up Arcane's built-in app-level providers for services,
|
||
feature flags, environment, and theme updates:
|
||
|
||
```dart
|
||
import 'package:arcane_framework/arcane_framework.dart';
|
||
|
||
void main() {
|
||
runApp(
|
||
ArcaneApp(
|
||
builder: (context, _) => MainApp(),
|
||
),
|
||
);
|
||
}
|
||
```
|
||
|
||
`ArcaneApp.child` remains available for backward compatibility, but is
|
||
deprecated in favor of `ArcaneApp.builder`.
|
||
|
||
### ArcaneApp Builder Migration (v1.x -> v2.x)
|
||
|
||
Arcane now prefers `ArcaneApp.builder` over `ArcaneApp.child`.
|
||
|
||
Why this is better:
|
||
|
||
- Your app root is built with Arcane providers already in scope.
|
||
- You can access Arcane-backed context values immediately at app-root build
|
||
time.
|
||
- You no longer need an extra `Builder` wrapper just to capture provider-aware
|
||
context.
|
||
|
||
When to use each API:
|
||
|
||
| Situation | Recommended API | Why |
|
||
| ---------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------ |
|
||
| New app code | `ArcaneApp.builder` | Preferred, future-facing API. |
|
||
| Root widget needs Arcane-aware `BuildContext` during construction | `ArcaneApp.builder` | Context is captured inside Arcane's provider tree. |
|
||
| Existing app already uses `child` and you want minimal churn right now | `ArcaneApp.child` (deprecated) | Still supported for compatibility while you migrate. |
|
||
| You only need to pass through a static root widget | `ArcaneApp.builder` | Keeps usage consistent with migration target and avoids later refactors. |
|
||
|
||
Migration example:
|
||
|
||
```dart
|
||
// Before (deprecated)
|
||
ArcaneApp(
|
||
child: MainApp(),
|
||
)
|
||
|
||
// After (preferred)
|
||
ArcaneApp(
|
||
builder: (context, _) => MainApp(),
|
||
)
|
||
```
|
||
|
||
### Arcane.log Metadata Migration (v1.x -> v2.x)
|
||
|
||
`Arcane.log(...)` now accepts `metadata` as `Map<String, Object?>?`.
|
||
This allows metadata values to be non-strings (for example `int`, `bool`,
|
||
lists, or nested maps).
|
||
|
||
Migration example:
|
||
|
||
```dart
|
||
// Before
|
||
Arcane.log(
|
||
"Login attempt",
|
||
metadata: {
|
||
"userId": userId.toString(),
|
||
"attempt": attempt.toString(),
|
||
"rememberMe": rememberMe.toString(),
|
||
},
|
||
);
|
||
|
||
// After
|
||
Arcane.log(
|
||
"Login attempt",
|
||
metadata: {
|
||
"userId": userId,
|
||
"attempt": attempt,
|
||
"rememberMe": rememberMe,
|
||
},
|
||
);
|
||
```
|
||
|
||
If your logger destination serializes metadata, ensure it can handle
|
||
`Object?` values (or convert values to strings at that boundary).
|
||
|
||
## Usage
|
||
|
||
The following sections provide more information about how to use the package's
|
||
available features.
|
||
|
||
### Services
|
||
|
||
The Arcane Framework provides a centralized way to manage services across your
|
||
application, while optionally leveraging a built-in service locator.
|
||
|
||
Unlike most of the features in Arcane, a _service_ is broadly user-defined. What
|
||
a service is, or does, is not rigorously enforced by the framework itself. What
|
||
an `ArcaneService` offers, however, is the ability to be registered (and
|
||
unregistered), as well as located via `BuildContext`. The locators are the key
|
||
value proposition that Arcane provides.
|
||
|
||
The following tools are provided by Arcane to assist with creating and using
|
||
services:
|
||
|
||
- `ArcaneService`: The base class from which to extend your own services. This
|
||
what Arcane uses to locate services.
|
||
- `ArcaneServiceProvider`: A widget used to provide access to registered
|
||
`ArcaneService` instances. **Note**: This widget is already part of the
|
||
_`ArcaneApp`_ widget, however if you are not using the `ArcaneApp` widget you
|
||
can instead use this widget directly.
|
||
- The `service<T>` and `requiredService<T>` extensions on `BuildContext`:
|
||
nullable and non-nullable getters used to locate a given `ArcaneService` via
|
||
`BuildContext`. **Note**: For app-defined services, these lookups require an
|
||
`ArcaneServiceProvider` in the widget tree. For Arcane built-in singleton
|
||
services (such as `Arcane.auth`, `Arcane.features`, `Arcane.theme`, and
|
||
`Arcane.environment`), lookups fall back to built-ins even when no provider
|
||
is available.
|
||
|
||
#### Defining an example `ArcaneService`
|
||
|
||
As noted previously, _what_ a service is or does is not enforced by the
|
||
framework. Therefore, the following example is only in service of the remainder
|
||
of the documentation of the Arcane services feature.
|
||
|
||
This example service is a singleton service that stores and provides access to a
|
||
user's favorite color, leveraging a `ValueNotifier` to trigger rebuilds as
|
||
appropriate:
|
||
|
||
```dart
|
||
class FavoriteColorService extends ArcaneService {
|
||
|
||
FavoriteColorService();
|
||
|
||
final ValueNotifier<Color?> _notifier = ValueNotifier<Color?>(null);
|
||
ValueNotifier<Color?> get notifier => _notifier;
|
||
|
||
Color? get myFavoriteColor => _notifier.value;
|
||
|
||
void setMyFavoriteColor(Color? color) {
|
||
if (_notifier.value != color) {
|
||
_notifier.value = color;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### Registering and unregistering an `ArcaneService`
|
||
|
||
The quickest and easiest way to register an `ArcaneService` is to use the
|
||
built-in `ArcaneApp` widget. However, this is not the _only_ method available.
|
||
|
||
To register your `ArcaneService` using an app with the `ArcaneApp` widget, you
|
||
have a couple of options. First, you can simply add the service (in our case, a
|
||
singleton instance) to the `services` list directly:
|
||
|
||
```dart
|
||
ArcaneApp(
|
||
services: [
|
||
FavoriteColorService(),
|
||
],
|
||
builder: (context, _) => MainApp(),
|
||
),
|
||
```
|
||
|
||
You can also defer adding the service by invoking `ArcaneServiceProvider`. Note
|
||
that this requires either `ArcaneServiceProvider` _or_ `ArcaneApp` (which
|
||
already includes `ArcaneServiceProvider`) to be in your widget tree.
|
||
|
||
```dart
|
||
// The service is not included at compile-time
|
||
ArcaneApp(
|
||
builder: (context, _) => MainApp(),
|
||
),
|
||
|
||
// Add the service at runtime
|
||
ArcaneServiceProvider.of(context).addService(FavoriteColorService());
|
||
```
|
||
|
||
Unregistering an already registered `ArcaneService` is as simple as:
|
||
|
||
```dart
|
||
ArcaneServiceProvider.of(context).removeService<FavoriteColorService>()
|
||
```
|
||
|
||
When both a provider-registered service and an Arcane built-in singleton match
|
||
the same requested type, provider-registered services take precedence for
|
||
`context.service<T>()` and `context.requiredService<T>()`.
|
||
|
||
#### Locating an `ArcaneService`
|
||
|
||
There are numerous ways to locate a registered `ArcaneService`. Feel free to use
|
||
whatever method you prefer:
|
||
|
||
```dart
|
||
// If a service of the given type is not registered, `null` is returned.
|
||
final FavoriteColorService? nullableService = ArcaneService.ofType<FavoriteColorService>(context);
|
||
final FavoriteColorService? nullableViaContext = context.service<FavoriteColorService>();
|
||
final FavoriteColorService? nullableViaProvider = ArcaneServiceProvider.serviceOfType<FavoriteColorService>(context);
|
||
|
||
// If a service of the given type is not registered, an exception is thrown.
|
||
final FavoriteColorService nonNullableService = ArcaneService.requiredOfType<FavoriteColorService>(context);
|
||
final FavoriteColorService nonNullableViaContext = context.requiredService<FavoriteColorService>();
|
||
final FavoriteColorService nonNullableViaProvider = ArcaneServiceProvider.requiredServiceOfType<FavoriteColorService>(context);
|
||
```
|
||
|
||
In addition, you can locate a `ArcaneServiceProvider` in a similar way:
|
||
|
||
```dart
|
||
// Returns `null` if no `ArcaneServiceProvider` is found in the widget tree.
|
||
final ArcaneServiceProvider? nullableProvider = ArcaneServiceProvider.maybeOf(context);
|
||
|
||
// Throws an exception if no `ArcaneServiceProvider` is found in the widget tree.
|
||
final ArcaneServiceProvider nonNullableProvider = ArcaneServiceProvider.of(context);
|
||
```
|
||
|
||
#### Using `ArcaneService` services
|
||
|
||
Since the `ArcaneService` class includes a `ChangeNotifier`, invoking the
|
||
`notifyListeners()` method inside a service will trigger a rebuild. Using our
|
||
`FavoriteColorService` from earlier, we can add a listener to our notifier
|
||
value:
|
||
|
||
```dart
|
||
final FavoriteColorService service = ArcaneService.requiredOfType<FavoriteColorService>(context);
|
||
|
||
service.notifier.addListener(() {
|
||
final Color? color = service.myFavoriteColor;
|
||
// Do something with our value
|
||
});
|
||
```
|
||
|
||
We can also simply use a `ValueListenableBuilder`:
|
||
|
||
```dart
|
||
ValueListenableBuilder(
|
||
valueListenable: ArcaneService.requiredOfType<FavoriteColorService>(context).notifier,
|
||
builder: (context, color, _) {
|
||
return Text("My favorite color is $color"),
|
||
}
|
||
)
|
||
```
|
||
|
||
Meanwhile, setting the value in our service can be accomplished in the following
|
||
manner:
|
||
|
||
```dart
|
||
ArcaneService.requiredOfType<FavoriteColorService>(context).setMyFavoriteColor(Colors.purple);
|
||
```
|
||
|
||
Again, this example is _not_ the only way the Arcane Service system can be
|
||
utilized. One is limited only by their imagination!
|
||
|
||
### Feature Flags
|
||
|
||
You can easily manage feature flags using the `ArcaneFeatureFlagService` built-in
|
||
service. Feature flags are useful for enabling or disabling different parts of
|
||
your application under different circumstances. For example, you may want to
|
||
enable a new feature only once it has finished development and testing, while
|
||
still having the ability to ship the unfinished code. You could also leverage
|
||
feature flags to enable different modes within your application (e.g., "free" vs
|
||
"paid"). Furthermore, they can be used for A/B testing. The options are truly
|
||
unlimited.
|
||
|
||
To get started, create an `enum` to define your features:
|
||
|
||
```dart
|
||
enum Feature {
|
||
awesomeFeature(true),
|
||
prettyOkFeature(false),
|
||
;
|
||
|
||
/// Determines whether the given [Feature] is enabled by default when the
|
||
/// application launches. Features can be enabled or disabled during runtime,
|
||
/// regardless of this value.
|
||
final bool enabledAtStartup;
|
||
|
||
const Feature(this.enabledAtStartup);
|
||
}
|
||
```
|
||
|
||
Next, ensure that your features are enabled at startup by registering them
|
||
within the feature flag service:
|
||
|
||
```dart
|
||
void main() {
|
||
WidgetsFlutterBinding.ensureInitialized();
|
||
|
||
// Register your Enum that you'll be using to enable and disable features.
|
||
for (final Feature feature in Feature.values) {
|
||
if (feature.enabledAtStartup) Arcane.features.enableFeature(feature);
|
||
}
|
||
|
||
runApp(
|
||
ArcaneApp(
|
||
builder: (context, _) => MainApp(),
|
||
),
|
||
);
|
||
}
|
||
```
|
||
|
||
When you want to determine if a feature is enabled, you can use one of the
|
||
helper extensions:
|
||
|
||
```dart
|
||
// Via an enum extension
|
||
final bool isMyAwesomeFeatureEnabled = Feature.awesomeFeature.enabled;
|
||
|
||
// Via the Arcane feature flag service
|
||
final bool isMyPrettyOkFeatureDisabled = Arcane.features.isDisabled(Feature.prettyOkFeature);
|
||
```
|
||
|
||
You can also enable and disable features at runtime:
|
||
|
||
```dart
|
||
// Via an enum extension
|
||
Feature.awesomeFeature.disable();
|
||
Feature.prettyOkFeature.enable();
|
||
|
||
// Via the Arcane features service
|
||
Arcane.features.disableFeature(Feature.awesomeFeature);
|
||
Arcane.features.enableFeature(Feature.prettyOkFeature);
|
||
```
|
||
|
||
To get a list of the currently enabled features, simply ask the Arcane feature
|
||
flag service:
|
||
|
||
```dart
|
||
final List<Enum> enabledFeatures = Arcane.features.enabledFeatures;
|
||
```
|
||
|
||
`enabledFeatures` is a snapshot read. Reading it does not subscribe to updates
|
||
and does not trigger widget rebuilds.
|
||
|
||
It is also possible to add a listener to watch for changes in the enabled
|
||
features.
|
||
|
||
```dart
|
||
Arcane.features.notifier.addListener(() {
|
||
print("Features changed: ${Arcane.features.enabledFeatures}");
|
||
});
|
||
```
|
||
|
||
If you prefer stream-based subscriptions, you can listen to
|
||
`enabledFeaturesChanges` and cancel the subscription in `dispose`.
|
||
|
||
```dart
|
||
late final StreamSubscription<List<Enum>> subscription;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
subscription = Arcane.features.enabledFeaturesChanges.listen((features) {
|
||
print("Features changed: $features");
|
||
});
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
subscription.cancel();
|
||
super.dispose();
|
||
}
|
||
```
|
||
|
||
When using `ArcaneApp`, you can also depend on feature flags via
|
||
`context.featureFlags`. This resolves to the nearest
|
||
`ArcaneFeatureFlagProvider` and widgets that read it rebuild automatically when
|
||
feature flags change.
|
||
|
||
```dart
|
||
class FeatureGate extends StatelessWidget {
|
||
const FeatureGate({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final flags = context.featureFlags;
|
||
if (flags.isDisabled(Feature.awesomeFeature)) {
|
||
return const SizedBox.shrink();
|
||
}
|
||
|
||
return const Text("Awesome feature enabled");
|
||
}
|
||
}
|
||
```
|
||
|
||
Additional `BuildContext` helpers are also available:
|
||
|
||
```dart
|
||
final ArcaneFeatureFlagProvider? maybeFlags = context.maybeFeatureFlags;
|
||
|
||
if (context.isFeatureEnabled(Feature.awesomeFeature)) {
|
||
// Feature is enabled
|
||
}
|
||
|
||
if (context.isFeatureDisabled(Feature.prettyOkFeature)) {
|
||
// Feature is disabled
|
||
}
|
||
```
|
||
|
||
Note that it is possible to register multiple different `Enum` types in the
|
||
feature flag service, should one have a need to do so.
|
||
|
||
### Logging
|
||
|
||
The Arcane Framework provides a robust logging system for your application. This
|
||
allows you to log messages with metadata, stack traces, and different log
|
||
levels while routing a single log event to multiple destinations.
|
||
|
||
To get started, first create one or more logging interfaces by extending
|
||
`LoggingInterface`.
|
||
|
||
```dart
|
||
class DebugConsole extends LoggingInterface {
|
||
@override
|
||
void log(
|
||
String message, {
|
||
Map<String, Object?>? metadata,
|
||
Level? level,
|
||
StackTrace? stackTrace,
|
||
Object? extra,
|
||
}) {
|
||
debugPrint(
|
||
"$message\n"
|
||
"$metadata\n",
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
If your destination needs setup (SDK start, permission checks, etc.), opt into
|
||
the initialization lifecycle with `LoggingInitialization`:
|
||
|
||
```dart
|
||
class ExternalLogger extends LoggingInterface with LoggingInitialization {
|
||
@override
|
||
Future<void> init() async {
|
||
if (initialized) return;
|
||
|
||
// Configure and start the SDK.
|
||
await super.init();
|
||
}
|
||
|
||
@override
|
||
void log(
|
||
String message, {
|
||
Map<String, Object?>? metadata,
|
||
Level? level,
|
||
StackTrace? stackTrace,
|
||
Object? extra,
|
||
}) {
|
||
if (!initialized) return;
|
||
// Forward to the SDK.
|
||
}
|
||
}
|
||
```
|
||
|
||
If you want to tag a destination, annotate the interface with
|
||
`@LoggingFeature(...)`:
|
||
|
||
```dart
|
||
@LoggingFeature("Analytics")
|
||
class AnalyticsLogger extends LoggingInterface {
|
||
|
||
@override
|
||
void log(
|
||
String message, {
|
||
Map<String, Object?>? metadata,
|
||
Level? level,
|
||
StackTrace? stackTrace,
|
||
Object? extra,
|
||
}) {
|
||
// Forward to analytics pipeline.
|
||
}
|
||
}
|
||
|
||
Arcane.logger.registerInterceptor(
|
||
LogInterceptor((event, context) {
|
||
if (context.interface is AnalyticsLogger && event.level == Level.debug) {
|
||
return null;
|
||
}
|
||
|
||
return event;
|
||
}),
|
||
);
|
||
```
|
||
|
||
You can use this tag as source-level documentation and keep destination routing
|
||
explicit in interceptors.
|
||
|
||
```dart
|
||
@LoggingFeature("auth")
|
||
class AuthLogger extends LoggingInterface {
|
||
@override
|
||
void log(
|
||
String message, {
|
||
Map<String, Object?>? metadata,
|
||
Level? level,
|
||
StackTrace? stackTrace,
|
||
Object? extra,
|
||
}) {
|
||
// Forward to auth destination.
|
||
}
|
||
}
|
||
|
||
await Arcane.logger.registerInterface(AnalyticsLogger());
|
||
await Arcane.logger.registerInterface(AuthLogger());
|
||
```
|
||
|
||
Next, register your logging interface with the Arcane logger service. You can
|
||
attach interceptors when registering an interface, or add global interceptors
|
||
later at runtime.
|
||
|
||
```dart
|
||
final DebugConsole debugConsole = DebugConsole();
|
||
|
||
await Arcane.logger.registerInterface(
|
||
debugConsole,
|
||
interceptors: [
|
||
LogInterceptor((event, context) {
|
||
if (context.interface is DebugConsole && event.level == Level.debug) {
|
||
return null;
|
||
}
|
||
|
||
return event;
|
||
}),
|
||
],
|
||
);
|
||
|
||
Arcane.logger.registerInterceptor(
|
||
LogInterceptor((event, context) {
|
||
return event.copyWith(
|
||
metadata: {
|
||
...?event.metadata,
|
||
"session": "startup",
|
||
},
|
||
);
|
||
}),
|
||
);
|
||
|
||
// Optional: initialize only interfaces that implement LoggingInitializable
|
||
// (for example, SDK-backed loggers that mix in LoggingInitialization).
|
||
await Arcane.logger.initializeInterfaces();
|
||
```
|
||
|
||
Global interceptors are evaluated for each registered interface, and interface
|
||
interceptors run immediately after them for that same destination. Every
|
||
interceptor receives a `LogInterceptorContext` whose `interface` value is the
|
||
current destination, which allows a single global interceptor to allow one
|
||
interface to receive an event while dropping it for another.
|
||
|
||
Returning `null` from an interceptor drops the event for the current scope.
|
||
Returning a modified `LogEvent` allows you to rewrite the message, metadata,
|
||
level, stack trace, or extra payload before it is logged.
|
||
|
||
Finally, add any additional persistent metadata to your log messages (optional)
|
||
and log a message:
|
||
|
||
```dart
|
||
// Add metadata to the logger
|
||
Arcane.logger.addPersistentMetadata({
|
||
"app_name": "My App",
|
||
"environment": "production",
|
||
});
|
||
|
||
// Log a message!
|
||
Arcane.log(
|
||
"This is a debug message",
|
||
level: Level.debug,
|
||
module: "ModuleName",
|
||
method: "MethodName",
|
||
metadata: {
|
||
"key": "value",
|
||
"attempt": 1,
|
||
"retryable": true,
|
||
},
|
||
stackTrace: StackTrace.current,
|
||
);
|
||
|
||
// Optional: skip automatic module/method/file-line detection.
|
||
Arcane.log(
|
||
"Manual log routing",
|
||
module: "CustomModule",
|
||
method: "customMethod",
|
||
skipAutodetection: true,
|
||
);
|
||
```
|
||
|
||
You can also listen to `logStream` for realtime log events, and cancel and
|
||
re-register subscribers as widget lifecycles change:
|
||
|
||
```dart
|
||
late final StreamSubscription<String> logSubscription;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
logSubscription = Arcane.logger.logStream.listen((message) {
|
||
debugPrint("Log stream event: $message");
|
||
});
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
logSubscription.cancel();
|
||
super.dispose();
|
||
}
|
||
```
|
||
|
||
You can also add and remove global interceptors after startup. Because every
|
||
interceptor receives a `LogInterceptorContext`, a single global interceptor can
|
||
still make interface-specific decisions by checking `context.interface`. If you
|
||
prefer, you can also define your own interceptor class by implementing
|
||
`LogInterceptor` instead of using the callback constructor.
|
||
|
||
```dart
|
||
final LogInterceptor redactSecrets = LogInterceptor((
|
||
event,
|
||
context,
|
||
) {
|
||
final Object? token = event.metadata?["token"];
|
||
if (token == null) return event;
|
||
|
||
return event.copyWith(
|
||
metadata: {
|
||
...?event.metadata,
|
||
"token": "[redacted]",
|
||
},
|
||
);
|
||
});
|
||
|
||
Arcane.logger.registerInterceptor(redactSecrets);
|
||
Arcane.logger.unregisterInterceptor(redactSecrets);
|
||
```
|
||
|
||
If you prefer a reusable named type, you can also implement `LogInterceptor`
|
||
directly:
|
||
|
||
```dart
|
||
class RedactingLogInterceptor implements LogInterceptor {
|
||
const RedactingLogInterceptor();
|
||
|
||
@override
|
||
LogEvent? call(
|
||
LogEvent event, {
|
||
required LogInterceptorContext context,
|
||
}) {
|
||
final Object? token = event.metadata?["token"];
|
||
if (token == null) return event;
|
||
|
||
return event.copyWith(
|
||
metadata: {
|
||
...?event.metadata,
|
||
"token": "[redacted]",
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
final LogInterceptor redactSecrets = RedactingLogInterceptor();
|
||
|
||
Arcane.logger.registerInterceptor(redactSecrets);
|
||
Arcane.logger.unregisterInterceptor(redactSecrets);
|
||
```
|
||
|
||
Multiple logging interfaces and multiple interceptors can be registered
|
||
simultaneously. Interface-specific interceptors receive copied `LogEvent`
|
||
instances, so mutations made for one destination do not leak into another.
|
||
|
||
**Important**: Initialization is now optional per interface. Call
|
||
`initializeInterfaces()` when you have interfaces that opt into
|
||
`LoggingInitializable` (for example via `LoggingInitialization`). Simple
|
||
destinations like a debug console can skip initialization entirely.
|
||
|
||
### Authentication
|
||
|
||
The Arcane Framework provides a useful interface for performing common
|
||
authentication tasks, such as registration, password resets, login, log out, and
|
||
enabling a debug mode.
|
||
|
||
To get started, create an authentication interface provider and register it in
|
||
the Arcane authentication module:
|
||
|
||
```dart
|
||
import "package:arcane_framework/arcane_framework.dart";
|
||
|
||
typedef Credentials = ({String email, String password});
|
||
|
||
class DebugAuthInterface
|
||
with ArcaneAuthAccountRegistration, ArcaneAuthPasswordManagement
|
||
implements ArcaneAuthInterface {
|
||
DebugAuthInterface._internal();
|
||
|
||
static final ArcaneAuthInterface _instance = DebugAuthInterface._internal();
|
||
static ArcaneAuthInterface get I => _instance;
|
||
|
||
@override
|
||
Future<bool> get isSignedIn => Future.value(_isSignedIn);
|
||
bool _isSignedIn = false;
|
||
|
||
@override
|
||
Future<String?> get accessToken => isSignedIn.then(
|
||
(loggedIn) => loggedIn ? "access_token" : null,
|
||
);
|
||
|
||
@override
|
||
Future<String?> get refreshToken => isSignedIn.then(
|
||
(loggedIn) => loggedIn ? "refresh_token" : null,
|
||
);
|
||
|
||
@override
|
||
Future<Result<void, String>> logout({
|
||
Future<void> Function()? onLoggedOut,
|
||
}) async {
|
||
Arcane.log("Logging out");
|
||
|
||
_isSignedIn = false;
|
||
|
||
if (onLoggedOut != null) await onLoggedOut();
|
||
|
||
return Result.ok(null);
|
||
}
|
||
|
||
@override
|
||
Future<Result<void, String>> login<Credentials>({
|
||
Credentials? input,
|
||
Future<void> Function()? onLoggedIn,
|
||
}) async {
|
||
final bool alreadyLoggedIn = await isSignedIn;
|
||
|
||
if (alreadyLoggedIn) return Result.ok(null);
|
||
|
||
final credentials = input as ({String email, String password});
|
||
|
||
final String email = credentials.email;
|
||
final String password = credentials.password;
|
||
|
||
Arcane.log("Logging in as $email using password $password");
|
||
|
||
_isSignedIn = true;
|
||
|
||
return Result.ok(null);
|
||
}
|
||
|
||
// Provided by the ArcaneAuthAccountRegistration mixin
|
||
@override
|
||
Future<Result<String, String>> resendVerificationCode<T>({
|
||
T? input,
|
||
}) async {
|
||
Arcane.log("Re-sending verification code to $input");
|
||
return Result.ok("Code sent");
|
||
}
|
||
|
||
// Provided by the ArcaneAuthAccountRegistration mixin
|
||
@override
|
||
Future<Result<SignUpStep, String>> register<Credentials>({
|
||
Credentials? input,
|
||
}) async {
|
||
if (input != null) {
|
||
final credentials = input as ({String email, String password});
|
||
|
||
final String email = credentials.email;
|
||
final String password = credentials.password;
|
||
|
||
Arcane.log("Creating account for $email with password $password");
|
||
}
|
||
|
||
return Result.ok(SignUpStep.confirmSignUp);
|
||
}
|
||
|
||
// Provided by the ArcaneAuthAccountRegistration mixin
|
||
@override
|
||
Future<Result<bool, String>> confirmSignup({
|
||
String? username,
|
||
String? confirmationCode,
|
||
}) async {
|
||
Arcane.log(
|
||
"Confirming registration for $username with code $confirmationCode",
|
||
);
|
||
return Result.ok(true);
|
||
}
|
||
|
||
// Provided by the ArcaneAuthPasswordManagement mixin
|
||
@override
|
||
Future<Result<bool, String>> resetPassword({
|
||
String? email,
|
||
String? newPassword,
|
||
String? code,
|
||
}) async {
|
||
Arcane.log("Resetting password for $email");
|
||
return Result.ok(true);
|
||
}
|
||
|
||
@override
|
||
Future<void> init() async {
|
||
Arcane.log("Debug auth interface initialized.");
|
||
return;
|
||
}
|
||
}
|
||
|
||
|
||
|
||
// Register an interface to handle user authentication.
|
||
await Arcane.auth.registerInterface(DebugAuthInterface.I);
|
||
```
|
||
|
||
Once your interface has been created and registered, you can use it to perform a
|
||
number of common authentication tasks:
|
||
|
||
```dart
|
||
// Register an account using the ArcaneAuthAccountRegistration mixin
|
||
final nextStep = await Arcane.auth.register<Credentials>(
|
||
input: (email: "user@example.com", password: "password123"),
|
||
);
|
||
|
||
// Confirm a newly registered account using the ArcaneAuthAccountRegistration mixin
|
||
final accountConfirmed = await Arcane.auth.confirmSignup(
|
||
email: "user@example.com",
|
||
confirmationCode: "123456",
|
||
);
|
||
|
||
// Re-send a verification code using the ArcaneAuthAccountRegistration mixin
|
||
final response = await Arcane.auth.resendVerificationCode("user@example.com");
|
||
|
||
// Initiate a password reset flow using the ArcaneAuthPasswordManagement mixin
|
||
final passwordResetStarted = await Arcane.auth.resetPassword(
|
||
email: "user@example.com",
|
||
newPassword: "password456",
|
||
);
|
||
|
||
// Confirm password reset using the ArcaneAuthPasswordManagement mixin
|
||
final passwordResetFinished = await Arcane.auth.resetPassword(
|
||
email: "user@example.com",
|
||
newPassword: "password456",
|
||
confirmationCode: "123456",
|
||
);
|
||
|
||
// Sign in with email and password
|
||
final result = await Arcane.auth.login(
|
||
input: (email: "user@example.com", password: "password123"),
|
||
onLoggedIn: () => Arcane.log("User logged in"),
|
||
);
|
||
|
||
// Sign out
|
||
await Arcane.auth.logOut();
|
||
```
|
||
|
||
Authentication updates can also be consumed through streams:
|
||
|
||
```dart
|
||
late final StreamSubscription<AuthenticationStatus> statusSubscription;
|
||
late final StreamSubscription<bool> signedInSubscription;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
|
||
statusSubscription = Arcane.auth.statusChanges.listen((status) {
|
||
debugPrint("Auth status changed: $status");
|
||
});
|
||
|
||
signedInSubscription = Arcane.auth.signedInChanges.listen((signedIn) {
|
||
debugPrint("Is signed in: $signedIn");
|
||
});
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
statusSubscription.cancel();
|
||
signedInSubscription.cancel();
|
||
super.dispose();
|
||
}
|
||
```
|
||
|
||
### Application Environments
|
||
|
||
Arcane environments are value-based and extensible. Two built-in values are
|
||
provided (`Environment.normal` and `Environment.debug`), and applications can define
|
||
their own environments (for example, `staging`).
|
||
|
||
```dart
|
||
const Environment staging = Environment("staging");
|
||
|
||
class EnvironmentSwitcher extends StatelessWidget {
|
||
const EnvironmentSwitcher({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final ArcaneEnvironmentService environment = Arcane.environment;
|
||
|
||
return ElevatedButton(
|
||
onPressed: () {
|
||
environment.setEnvironment(staging);
|
||
},
|
||
child: const Text("Use staging"),
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
`enableDebugMode()` and `disableDebugMode()` are still available convenience
|
||
helpers that map to the built-in debug and normal environments.
|
||
|
||
`ArcaneEnvironment` and `ArcaneEnvironmentProvider` are still available for
|
||
backward compatibility, but are deprecated in favor of
|
||
`Arcane.environment`/`ArcaneEnvironmentService`.
|
||
|
||
Authentication status is intentionally separate from environment. Switching
|
||
environments does not change `AuthenticationStatus`.
|
||
|
||
### Dynamic Theming
|
||
|
||
The Arcane Framework provides a simple interface for managing themes in your
|
||
application, with dynamic switching between dark and light themes based on the
|
||
user's system settings, or manually switching between themes.
|
||
|
||
To get started, first register your `ThemeData` objects with the Arcane theme
|
||
module:
|
||
|
||
```dart
|
||
void main() {
|
||
// Set your Themes
|
||
Arcane.theme
|
||
..setDarkTheme(darkTheme)
|
||
..setLightTheme(lightTheme);
|
||
|
||
runApp(
|
||
ArcaneApp(
|
||
builder: (context, _) => MainApp(),
|
||
),
|
||
);
|
||
}
|
||
```
|
||
|
||
From here, you can either follow the system theme:
|
||
|
||
```dart
|
||
// ArcaneApp already enables system-follow behavior by default.
|
||
class MainApp extends StatelessWidget {
|
||
const MainApp({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return MaterialApp(
|
||
theme: Arcane.theme.light,
|
||
darkTheme: Arcane.theme.dark,
|
||
themeMode: Arcane.theme.currentModeOf(context),
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
You can also manually control the theme mode:
|
||
|
||
```dart
|
||
// Manually control the theme mode
|
||
class MainApp extends StatelessWidget {
|
||
const MainApp({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return MaterialApp(
|
||
theme: Arcane.theme.light,
|
||
darkTheme: Arcane.theme.dark,
|
||
themeMode: Arcane.theme.currentModeOf(context),
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
Then, you can switch modes whenever you want:
|
||
|
||
```dart
|
||
// Switch between light and dark themes
|
||
Arcane.theme.switchTheme();
|
||
|
||
// Access current theme data
|
||
final ThemeData currentTheme = Arcane.theme.currentThemeMode == ThemeMode.dark
|
||
? Arcane.theme.dark
|
||
: Arcane.theme.light;
|
||
|
||
if (context.isDarkMode) {
|
||
// Do something when dark mode is active
|
||
}
|
||
|
||
// Set a custom dark theme
|
||
Arcane.theme.setDarkTheme(customDarkTheme);
|
||
|
||
// Equivalent assignment-style setter
|
||
Arcane.theme.dark = customDarkTheme;
|
||
|
||
// Set a custom light theme
|
||
Arcane.theme.setLightTheme(customLightTheme);
|
||
|
||
// Equivalent assignment-style setter
|
||
Arcane.theme.light = customLightTheme;
|
||
```
|
||
|
||
You can subscribe to theme streams to react to theme updates outside of widget
|
||
build methods:
|
||
|
||
```dart
|
||
late final StreamSubscription<ThemeMode> modeSubscription;
|
||
late final StreamSubscription<ThemeData> themeSubscription;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
modeSubscription = Arcane.theme.themeModeChanges.listen((mode) {
|
||
debugPrint("Theme mode changed: $mode");
|
||
});
|
||
themeSubscription = Arcane.theme.themeDataChanges.listen((themeData) {
|
||
debugPrint("Theme data changed: ${themeData.brightness}");
|
||
});
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
modeSubscription.cancel();
|
||
themeSubscription.cancel();
|
||
super.dispose();
|
||
}
|
||
```
|
||
|
||
## Contributing
|
||
|
||
We welcome contributions to the Arcane Framework. If you’d like to contribute,
|
||
please:
|
||
|
||
1. Fork the repository.
|
||
2. Create a new feature branch.
|
||
3. Submit a pull request with a description of your changes.
|
||
|
||
For detailed information on how to contribute, please refer to CONTRIBUTING.md.
|