mirror of
https://github.com/hanskokx/arcane_framework.git
synced 2026-05-14 02:19:08 +02:00
+32
@@ -0,0 +1,32 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
/pubspec.lock
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
build/
|
||||
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
@@ -0,0 +1,10 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "4cf269e36de2573851eaef3c763994f8f9be494d"
|
||||
channel: "stable"
|
||||
|
||||
project_type: package
|
||||
@@ -0,0 +1,3 @@
|
||||
## 1.0.0
|
||||
|
||||
* Initial release
|
||||
@@ -0,0 +1,13 @@
|
||||
Contributing
|
||||
============
|
||||
|
||||
[Improvements, bug reports, feature requests welcome][gh-issues].
|
||||
|
||||
- Please include `dart --version` and the package version when reporting bugs.
|
||||
- Code should be formatted with `dartfmt`.
|
||||
- Public methods should have doc comments and test coverage.
|
||||
- Itemize user-facing changes in the `HEAD` section of the `CHANGELOG` file.
|
||||
- Use [well-formatted commit messages][git-log-fmt].
|
||||
|
||||
[gh-issues]: https://github.com/hanskokx/arcane_framework/issues
|
||||
[git-log-fmt]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
|
||||
@@ -0,0 +1,608 @@
|
||||
The Arcane Framework is a powerful Dart package designed to provide a robust architecture for managing key application services such as logging, authentication, secure storage, feature flags, theming, and more. This framework is ideal for building scalable applications that require dynamic configuration and service management.
|
||||
|
||||
## Features
|
||||
|
||||
- **Service Management**: Centralized access to multiple services (logging, authentication, theming, etc.).
|
||||
- **Feature Flags**: Dynamically enable or disable features using `ArcaneFeatureFlags`.
|
||||
- **Logging**: Easily log messages with metadata, stack traces, and different log levels via `ArcaneLogger`.
|
||||
- **Authentication**: Built-in support for handling user authentication workflows.
|
||||
- **Theming**: Switch between light and dark themes with `ArcaneReactiveTheme`.
|
||||
- **Secure Storage**: Store sensitive data like tokens and user information securely using `ArcaneSecureStorage`.
|
||||
- **Session and Install ID Management**: Manage unique identifiers for user sessions and app installs.
|
||||
|
||||
## Getting Started
|
||||
|
||||
To use Arcane Framework in your Dart or Flutter project, follow these steps:
|
||||
|
||||
### Installation
|
||||
|
||||
1. Add the dependency to your pubspec.yaml:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
arcane_framework: <latest>
|
||||
```
|
||||
|
||||
2. Wrap your `MaterialApp` or `CupertinoApp` with the `ArcaneApp` Widget, providing the necessary services and your root widget.
|
||||
|
||||
```dart
|
||||
import 'package:arcane_framework/arcane_framework.dart';
|
||||
|
||||
void main() {
|
||||
runApp(
|
||||
ArcaneApp(
|
||||
services: [
|
||||
MyArcaneService.I,
|
||||
],
|
||||
child: MyApp(...),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The following sections provide more information about how to use the framework features.
|
||||
|
||||
### Services
|
||||
|
||||
The Arcane Framework provides a centralized way to manage services across your application. This allows you to easily access and configure all of your services from anywhere in your app, without having to pass them down through multiple widgets.
|
||||
|
||||
A service's purpose is to facilitate cross-feature communication of small pieces of data. For example, one feature may ask a user for their favorite color, while another feature may use that color to change the background of a screen. The feature ingesting the users' favorite color should not care how the favorite color has been determined, nor should it rely directly upon the feature that determines said color. A service can be used to hold the color in question, effectively decoupling these two features. One service sets the value while another ingests it.
|
||||
|
||||
```dart
|
||||
class FavoriteColorService extends ArcaneService {
|
||||
static bool _mocked = false;
|
||||
static final FavoriteColorService _instance = FavoriteColorService._internal();
|
||||
|
||||
static FavoriteColorService get I => _instance;
|
||||
|
||||
FavoriteColorService._internal();
|
||||
|
||||
Color? _myFavoriteColor;
|
||||
Color? get myFavoriteColor => _myFavoriteColor;
|
||||
|
||||
void setMyFavoriteColor(Color? newValue) {
|
||||
if (_mocked) return;
|
||||
|
||||
_myFavoriteColor = newValue;
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
static void setMocked() => _mocked = true;
|
||||
}
|
||||
```
|
||||
|
||||
To register a service with Arcane, simply add the instance of the `ArcaneService` to your list of services when initializing the `ArcaneApp`.
|
||||
|
||||
```dart
|
||||
ArcaneApp(
|
||||
services: [
|
||||
FavoriteColorService.I,
|
||||
],
|
||||
child: MyApp(...),
|
||||
),
|
||||
```
|
||||
|
||||
Service properties can be accessed either directly (e.g., `FavoriteColorService.I.myFavoriteColor`) or via `BuildContext` (e.g., `context.serviceOfType<FavoriteColorService>()?.myFavoriteColor`). If the `notifyListeners()` method is included within your service, any widgets that are referencing the service property through `BuildContext` will automatically be notified of the change.
|
||||
|
||||
### Feature Flags
|
||||
|
||||
You can easily manage feature flags using the `ArcaneFeatureFlags` 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(const ArcaneApp());
|
||||
}
|
||||
```
|
||||
|
||||
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;
|
||||
```
|
||||
|
||||
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 easily log messages with metadata, stack traces, and different log levels. The framework also provides an easy way to configure the logger's behavior (e.g., whether or not to show stack traces).
|
||||
|
||||
To get started, first create one or more logging interfaces, extending the `LoggingInterface` base class.
|
||||
|
||||
```dart
|
||||
class DebugConsole implements LoggingInterface {
|
||||
static final DebugConsole _instance = DebugConsole._internal();
|
||||
static DebugConsole get I => _instance;
|
||||
|
||||
final bool _initialized = true;
|
||||
|
||||
@override
|
||||
bool get initialized => I._initialized;
|
||||
|
||||
DebugConsole._internal();
|
||||
|
||||
@visibleForTesting
|
||||
void setMocked() => _mocked = true;
|
||||
bool _mocked = false;
|
||||
|
||||
@override
|
||||
void log(
|
||||
String message, {
|
||||
Map<String, dynamic>? metadata,
|
||||
Level? level,
|
||||
StackTrace? stackTrace,
|
||||
}) {
|
||||
if (Feature.logging.disabled) return;
|
||||
if (Feature.debugConsoleLogging.disabled) return;
|
||||
|
||||
final Map<String, dynamic> localMetadata = metadata ?? {};
|
||||
|
||||
final String? module = localMetadata["module"] as String?;
|
||||
final String? method = localMetadata["method"] as String?;
|
||||
|
||||
const JsonEncoder encoder = JsonEncoder.withIndent(" ");
|
||||
final String? prettyprint =
|
||||
(localMetadata.isNotEmpty) ? encoder.convert(localMetadata) : null;
|
||||
|
||||
final Logger logger = Logger(
|
||||
level: level,
|
||||
printer: PrettyPrinter(
|
||||
methodCount: 2,
|
||||
errorMethodCount: kDebugMode &&
|
||||
!(level == Level.error ||
|
||||
level == Level.warning ||
|
||||
level == Level.trace ||
|
||||
level == Level.fatal)
|
||||
? 4
|
||||
: 8,
|
||||
stackTraceBeginIndex: 1,
|
||||
lineLength: 120,
|
||||
colors: !Platform.isIOS,
|
||||
printEmojis: kDebugMode,
|
||||
dateTimeFormat: DateTimeFormat.none,
|
||||
),
|
||||
);
|
||||
|
||||
// Print the message to the debug console
|
||||
String prefix = "";
|
||||
if (module != null) prefix += "[$module]";
|
||||
if (method != null) prefix += "[$method]";
|
||||
if (prefix.isNotEmpty) prefix += " ";
|
||||
message = "$prefix$message";
|
||||
|
||||
if (prettyprint.isNotNullOrEmpty) message += "\n\n$prettyprint";
|
||||
|
||||
localMetadata.removeWhere((key, value) => key == "module");
|
||||
localMetadata.removeWhere((key, value) => key == "method");
|
||||
localMetadata.removeWhere((key, value) => key == "filenameAndLineNumber");
|
||||
|
||||
logger.log(
|
||||
level ?? Level.debug,
|
||||
message,
|
||||
error: localMetadata["error"] ?? "",
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LoggingInterface?> init() async {
|
||||
if (_mocked) return null;
|
||||
|
||||
return I;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Next, register your logging interface with the Arcane logger service:
|
||||
|
||||
```dart
|
||||
// Register your logging interface(s)
|
||||
await Arcane.logger.registerInterfaces([
|
||||
DebugConsole.I,
|
||||
]);
|
||||
|
||||
// Initialize app tracking (recommended for iOS)
|
||||
await Arcane.logger.initalizeAppTracking(
|
||||
trackingDialog: () async {
|
||||
if (Arcane.logger.trackingStatus != TrackingStatus.notDetermined) return;
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: "Incoming tracking consent dialog",
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
await Arcane.logger.initializeInterfaces();
|
||||
},
|
||||
child: Text("Continue"),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// Initialize registered logging interfaces
|
||||
// NOTE: This step may be deferred until a user has consented to app tracking. It is perofrmed automatically when called via `initializeAppTracking`.
|
||||
await Arcane.logger.initializeInterfaces();
|
||||
```
|
||||
|
||||
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"},
|
||||
stackTrace: StackTrace.current,
|
||||
);
|
||||
```
|
||||
|
||||
Multiple logging interfaces can be registered simultaneously.
|
||||
|
||||
**Important**: Logging interfaces should generally be initialized after being registered with the logger service. This ensures that all logging interfaces are properly initialized before any messages are logged. This should typically be done manually in order to properly present the user with a message stating that they're about to be prompted for tracking permissions (on iOS). See the Arcane logger documentation for further details.
|
||||
|
||||
### 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
|
||||
// Create an authentication interface
|
||||
class AuthProviderInterface implements ArcaneAuthInterface {
|
||||
AuthProviderInterface._internal();
|
||||
|
||||
static bool _mocked = false;
|
||||
|
||||
static final AuthProviderInterface _instance = AuthProviderInterface._internal();
|
||||
static AuthProviderInterface get I => _instance;
|
||||
|
||||
Future<AuthSession?> get _session async {
|
||||
return await ThirdPartyAuthProvider.fetchAuthSession();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> get isSignedIn =>
|
||||
_session.then((value) => value?.isSignedIn == true);
|
||||
|
||||
@override
|
||||
Future<String?> get accessToken => isSignedIn.then(
|
||||
(loggedIn) => loggedIn
|
||||
? _session.then(
|
||||
(value) => value?.accessToken,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<String?> get refreshToken => isSignedIn.then(
|
||||
(loggedIn) => loggedIn
|
||||
? _session.then(
|
||||
(value) => value?.refreshToken,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<Result<void, String>> logout() async {
|
||||
final result = await _session.signOut();
|
||||
|
||||
if (result is FailedSignOut) {
|
||||
return Result.error(result.exception.message);
|
||||
}
|
||||
|
||||
return Result.ok(null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<void, String>> loginWithEmailAndPassword({
|
||||
required String email,
|
||||
required String password,
|
||||
}) async {
|
||||
final bool alreadyLoggedIn = await isSignedIn;
|
||||
|
||||
if (alreadyLoggedIn) return Result.ok(null);
|
||||
|
||||
try {
|
||||
final SignInResult result = await _session.signIn(
|
||||
username: email,
|
||||
password: password,
|
||||
);
|
||||
return Result.ok(null);
|
||||
} on AuthException catch (e) {
|
||||
return Result.error("Error signing in: ${e.message}");
|
||||
} catch (e) {
|
||||
return Result.error("Error signing in: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<String, String>> resendVerificationCode(String email) async {
|
||||
try {
|
||||
final result = await _session.resendSignUpCode(username: email);
|
||||
return Result.ok(result.message);
|
||||
} catch (e) {
|
||||
return Result.error("Error resending verification code: ${e.message}");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<SignUpStep, String>> signup({
|
||||
required String password,
|
||||
required String email,
|
||||
}) async {
|
||||
try {
|
||||
final SignUpResult result = await _session.signUp(
|
||||
username: email,
|
||||
password: password,
|
||||
);
|
||||
|
||||
if (result.nextStep.signUpStep == AuthSignUpStep.confirmSignUp) {
|
||||
return Result.ok(SignUpStep.confirmSignUp);
|
||||
}
|
||||
|
||||
return Result.ok(SignUpStep.done);
|
||||
} catch (e) {
|
||||
return Result.error("Error signing up user: ${e.message}");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<bool, String>> confirmSignup({
|
||||
required String username,
|
||||
required String confirmationCode,
|
||||
}) async {
|
||||
try {
|
||||
final SignUpResult result = await _session.confirmSignUp(
|
||||
username: username,
|
||||
confirmationCode: confirmationCode,
|
||||
);
|
||||
|
||||
return Result.ok(result.isSignUpComplete);
|
||||
} on AuthException catch (e) {
|
||||
return Result.error("Error confirming user: ${e.message}");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<bool, String>> resetPassword({
|
||||
required String email,
|
||||
String? newPassword,
|
||||
String? code,
|
||||
}) async {
|
||||
try {
|
||||
late ResetPasswordResult result;
|
||||
if (newPassword != null && code != null) {
|
||||
result = await _session.confirmResetPassword(
|
||||
username: email,
|
||||
newPassword: newPassword,
|
||||
confirmationCode: code,
|
||||
);
|
||||
}
|
||||
|
||||
if (newPassword == null && code == null) {
|
||||
result = await _session.resetPassword(
|
||||
username: email,
|
||||
);
|
||||
}
|
||||
|
||||
return Result.ok(result.isPasswordReset);
|
||||
} catch (e) {
|
||||
return Result.error("Error resetting the password: ${e.message}");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> init() async {
|
||||
if (_mocked) return;
|
||||
|
||||
if (ThirdPartyAuthProvider.isConfigured) return;
|
||||
|
||||
await ThirdPartyAuthProvider.initialize();
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
static void setMocked() {
|
||||
_mocked = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Register an interface to handle user authentication.
|
||||
await Arcane.auth.registerInterface(AuthProviderInterface.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
|
||||
final nextStep = await Arcane.auth.signup(
|
||||
email: "user@example.com",
|
||||
password: "password123",
|
||||
);
|
||||
|
||||
// Confirm a newly registered account
|
||||
final accountConfirmed = await Arcane.auth.confirmSignup(
|
||||
email: "user@example.com",
|
||||
confirmationCode: "123456",
|
||||
);
|
||||
|
||||
// Re-send a verification code
|
||||
final response = await Arcane.auth.resendVerificationCode("user@example.com");
|
||||
|
||||
// Initiate a password reset flow
|
||||
final passwordResetStarted = await Arcane.auth.resetPassword(
|
||||
email: "user@example.com",
|
||||
newPassword: "password456",
|
||||
);
|
||||
|
||||
// Confirm password reset
|
||||
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.loginWithEmailAndPassword(
|
||||
email: "user@example.com",
|
||||
password: "password123",
|
||||
);
|
||||
|
||||
// Sign out
|
||||
await Arcane.auth.logout();
|
||||
|
||||
// Set the system to debug mode
|
||||
await Arcane.auth.setDebug();
|
||||
```
|
||||
|
||||
### 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, then reference the theme in your `MaterialApp` or `CupertinoApp`:
|
||||
|
||||
```dart
|
||||
void main() {
|
||||
// Set your Themes
|
||||
Arcane.theme
|
||||
..setDarkTheme(darkTheme)
|
||||
..setLightTheme(lightTheme);
|
||||
|
||||
runApp(
|
||||
ArcaneApp(
|
||||
child: MaterialApp(
|
||||
theme: Arcane.theme.light,
|
||||
darkTheme: Arcane.theme.dark,
|
||||
themeMode: context.isDarkMode ? ThemeMode.dark : ThemeMode.light,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Once configured, you'll have access to theme-related methods and properties:
|
||||
|
||||
```dart
|
||||
// Switch between light and dark themes
|
||||
Arcane.theme.switchTheme(context);
|
||||
|
||||
// Access current theme data
|
||||
final ThemeData currentTheme = Arcane.theme.currentMode == 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);
|
||||
|
||||
// Set a custom light theme
|
||||
Arcane.theme.setLightTheme(customLightTheme);
|
||||
```
|
||||
|
||||
### Secure Storage
|
||||
|
||||
You can store sensitive data like tokens securely with `ArcaneSecureStorage`:
|
||||
|
||||
```dart
|
||||
// Store a value in secure storage
|
||||
await Arcane.storage.setValue(ArcaneSecureStorage.emailKey, "user@example.com");
|
||||
|
||||
// Retrieve a value from secure storage
|
||||
final email = await Arcane.storage.getValue(ArcaneSecureStorage.emailKey);
|
||||
|
||||
// Delete all stored data
|
||||
await Arcane.storage.deleteAll();
|
||||
```
|
||||
|
||||
### Install and Session IDs
|
||||
|
||||
Use `ArcaneIdService` to manage unique IDs for app installations and user sessions:
|
||||
|
||||
```dart
|
||||
// Get the install ID
|
||||
final installId = await Arcane.id.installId;
|
||||
|
||||
// Get the session ID
|
||||
final sessionId = await Arcane.id.sessionId;
|
||||
|
||||
// Generate a new ID
|
||||
final newId = Arcane.id.newId;
|
||||
```
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,117 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
analyzer:
|
||||
errors:
|
||||
# treat missing required parameters as an error (not a hint)
|
||||
missing_required_param: error
|
||||
# treat missing returns as an error (not a hint)
|
||||
missing_return: error
|
||||
invalid_annotation_target: ignore
|
||||
language:
|
||||
strict-casts: true
|
||||
|
||||
linter:
|
||||
rules:
|
||||
always_declare_return_types: true
|
||||
always_put_required_named_parameters_first: true
|
||||
annotate_overrides: true
|
||||
avoid_annotating_with_dynamic: true
|
||||
avoid_dynamic_calls: true
|
||||
avoid_escaping_inner_quotes: true
|
||||
avoid_function_literals_in_foreach_calls: true
|
||||
avoid_null_checks_in_equality_operators: true
|
||||
avoid_print: true
|
||||
avoid_relative_lib_imports: true
|
||||
avoid_setters_without_getters: true
|
||||
avoid_shadowing_type_parameters: true
|
||||
avoid_single_cascade_in_expression_statements: true
|
||||
avoid_unnecessary_containers: true
|
||||
avoid_unused_constructor_parameters: true
|
||||
avoid_void_async: true
|
||||
camel_case_extensions: true
|
||||
camel_case_types: true
|
||||
cancel_subscriptions: true
|
||||
close_sinks: true
|
||||
collection_methods_unrelated_type: true
|
||||
constant_identifier_names: true
|
||||
control_flow_in_finally: true
|
||||
depend_on_referenced_packages: true
|
||||
directives_ordering: true
|
||||
empty_constructor_bodies: true
|
||||
empty_statements: true
|
||||
eol_at_end_of_file: true
|
||||
exhaustive_cases: true
|
||||
file_names: true
|
||||
flutter_style_todos: true
|
||||
hash_and_equals: true
|
||||
implementation_imports: true
|
||||
implicit_call_tearoffs: true
|
||||
leading_newlines_in_multiline_strings: true
|
||||
missing_whitespace_between_adjacent_strings: true
|
||||
no_adjacent_strings_in_list: true
|
||||
no_duplicate_case_values: true
|
||||
no_leading_underscores_for_library_prefixes: true
|
||||
no_leading_underscores_for_local_identifiers: true
|
||||
no_logic_in_create_state: true
|
||||
no_runtimeType_toString: true
|
||||
non_constant_identifier_names: true
|
||||
null_check_on_nullable_type_parameter: true
|
||||
null_closures: true
|
||||
only_throw_errors: true
|
||||
package_prefixed_library_names: true
|
||||
prefer_adjacent_string_concatenation: true
|
||||
prefer_asserts_in_initializer_lists: true
|
||||
prefer_collection_literals: true
|
||||
prefer_conditional_assignment: true
|
||||
prefer_const_constructors_in_immutables: true
|
||||
prefer_const_constructors: true
|
||||
prefer_const_declarations: true
|
||||
prefer_const_literals_to_create_immutables: true
|
||||
prefer_constructors_over_static_methods: true
|
||||
prefer_contains: true
|
||||
prefer_double_quotes: true
|
||||
prefer_final_fields: true
|
||||
prefer_final_in_for_each: true
|
||||
prefer_final_locals: true
|
||||
prefer_for_elements_to_map_fromIterable: true
|
||||
prefer_function_declarations_over_variables: true
|
||||
prefer_generic_function_type_aliases: true
|
||||
prefer_if_null_operators: true
|
||||
prefer_initializing_formals: true
|
||||
prefer_inlined_adds: true
|
||||
prefer_interpolation_to_compose_strings: true
|
||||
prefer_is_empty: true
|
||||
prefer_is_not_empty: true
|
||||
prefer_is_not_operator: true
|
||||
prefer_iterable_whereType: true
|
||||
prefer_null_aware_operators: true
|
||||
prefer_spread_collections: true
|
||||
prefer_typing_uninitialized_variables: true
|
||||
provide_deprecation_message: true
|
||||
recursive_getters: true
|
||||
require_trailing_commas: true
|
||||
sized_box_for_whitespace: true
|
||||
sized_box_shrink_expand: true
|
||||
slash_for_doc_comments: true
|
||||
sort_child_properties_last: true
|
||||
sort_pub_dependencies: true
|
||||
type_init_formals: true
|
||||
type_literal_in_constant_pattern: true
|
||||
unawaited_futures: true
|
||||
unnecessary_await_in_return: true
|
||||
unnecessary_brace_in_string_interps: true
|
||||
unnecessary_const: true
|
||||
unnecessary_constructor_name: true
|
||||
unnecessary_getters_setters: true
|
||||
unnecessary_late: true
|
||||
unnecessary_new: true
|
||||
unnecessary_null_aware_assignments: true
|
||||
unnecessary_null_in_if_null_operators: true
|
||||
unnecessary_nullable_for_final_variable_declarations: true
|
||||
unnecessary_overrides: true
|
||||
unnecessary_parenthesis: true
|
||||
unnecessary_statements: true
|
||||
use_build_context_synchronously: true
|
||||
use_colored_box: true
|
||||
use_key_in_widget_constructors: true
|
||||
valid_regexps: true
|
||||
@@ -0,0 +1,14 @@
|
||||
library arcane_framework;
|
||||
|
||||
export "package:app_tracking_transparency/app_tracking_transparency.dart";
|
||||
export "package:arcane_framework/src/arcane.dart";
|
||||
export "package:arcane_framework/src/arcane_app.dart";
|
||||
export "package:arcane_framework/src/providers/environment_provider.dart";
|
||||
export "package:arcane_framework/src/providers/service_provider.dart";
|
||||
export "package:arcane_framework/src/services/authentication/authentication_service.dart";
|
||||
export "package:arcane_framework/src/services/feature_flags/feature_flags_service.dart";
|
||||
export "package:arcane_framework/src/services/id/id_service.dart";
|
||||
export "package:arcane_framework/src/services/logging/logging_service.dart";
|
||||
export "package:arcane_framework/src/services/reactive_theme/reactive_theme_service.dart";
|
||||
export "package:arcane_framework/src/services/secure_storage/secure_storage_service.dart";
|
||||
export "package:result_monad/result_monad.dart";
|
||||
@@ -0,0 +1,96 @@
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
|
||||
/// A singleton class that acts as the central hub for various services in the
|
||||
/// Arcane framework.
|
||||
///
|
||||
/// `Arcane` provides access to important services like logging, feature flags,
|
||||
/// authentication, theming, secure storage, and ID management. It also offers a
|
||||
/// convenient method for logging messages using the integrated logger.
|
||||
class Arcane {
|
||||
Arcane._internal();
|
||||
|
||||
/// Creates a singleton instance of `Arcane`.
|
||||
///
|
||||
/// This factory constructor always returns the same instance of `Arcane`.
|
||||
factory Arcane() => Arcane._internal();
|
||||
|
||||
/// Provides access to the singleton instance of the logger service.
|
||||
///
|
||||
/// The `ArcaneLogger` is used for logging messages throughout the app.
|
||||
static ArcaneLogger get logger => ArcaneLogger.I;
|
||||
|
||||
/// Provides access to the singleton instance of the feature flags service.
|
||||
///
|
||||
/// `ArcaneFeatureFlags` manages feature toggles, allowing you to enable or
|
||||
/// disable features dynamically.
|
||||
static ArcaneFeatureFlags get features => ArcaneFeatureFlags.I;
|
||||
|
||||
/// Provides access to the singleton instance of the authentication service.
|
||||
///
|
||||
/// `ArcaneAuthenticationService` manages user authentication, login, and
|
||||
/// signup processes.
|
||||
static ArcaneAuthenticationService get auth => ArcaneAuthenticationService.I;
|
||||
|
||||
/// Provides access to the singleton instance of the theme management service.
|
||||
///
|
||||
/// `ArcaneReactiveTheme` allows switching between light and dark themes and
|
||||
/// customizing them.
|
||||
static ArcaneReactiveTheme get theme => ArcaneReactiveTheme.I;
|
||||
|
||||
/// Provides access to the singleton instance of the secure storage service.
|
||||
///
|
||||
/// `ArcaneSecureStorage` is responsible for securely storing key-value pairs,
|
||||
/// such as tokens or user data.
|
||||
static ArcaneSecureStorage get storage => ArcaneSecureStorage.I;
|
||||
|
||||
/// Provides access to the singleton instance of the ID service.
|
||||
///
|
||||
/// `ArcaneIdService` manages unique identifiers for installations and sessions.
|
||||
static ArcaneIdService get id => ArcaneIdService.I;
|
||||
|
||||
/// Returns a list of all services available in the Arcane framework.
|
||||
///
|
||||
/// This list includes the feature flags, authentication, theme, and ID services.
|
||||
static List<ArcaneService> get services => [
|
||||
features,
|
||||
auth,
|
||||
theme,
|
||||
id,
|
||||
];
|
||||
|
||||
/// Logs a message using the integrated logger.
|
||||
///
|
||||
/// This method is a convenient way to log messages with optional module,
|
||||
/// method, log level, stack trace, and additional metadata. The default log
|
||||
/// level is `Level.debug`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// Arcane.log("This is a log message", module: "MyModule", method: "MyMethod");
|
||||
/// ```
|
||||
///
|
||||
/// - [message]: The message to log.
|
||||
/// - [module]: Optional name of the module from which the log originated.
|
||||
/// - [method]: Optional name of the method from which the log originated.
|
||||
/// - [level]: The log level (e.g., `Level.debug`, `Level.error`), defaults to
|
||||
/// `Level.debug`.
|
||||
/// - [stackTrace]: Optional stack trace information.
|
||||
/// - [metadata]: Optional additional metadata in key-value pairs.
|
||||
static void log(
|
||||
String message, {
|
||||
String? module,
|
||||
String? method,
|
||||
Level level = Level.debug,
|
||||
StackTrace? stackTrace,
|
||||
Map<String, String>? metadata,
|
||||
}) {
|
||||
ArcaneLogger.I.log(
|
||||
message,
|
||||
module: module,
|
||||
method: method,
|
||||
level: level,
|
||||
stackTrace: stackTrace,
|
||||
metadata: metadata,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
/// A root widget for an Arcane-powered application.
|
||||
///
|
||||
/// `ArcaneApp` serves as the entry point for an application using the Arcane
|
||||
/// framework. It provides access to the application's services and environment
|
||||
/// settings throughout the widget tree using the `ArcaneServiceProvider` and
|
||||
/// `ArcaneEnvironmentProvider`.
|
||||
///
|
||||
/// This widget wraps the provided [child] widget with the necessary providers
|
||||
/// to make the Arcane services available to all descendant widgets.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// ArcaneApp(
|
||||
/// services: [ArcaneAuthenticationService(), ArcaneFeatureFlags()],
|
||||
/// child: MyApp(),
|
||||
/// );
|
||||
/// ```
|
||||
class ArcaneApp extends StatelessWidget {
|
||||
/// A list of Arcane services that will be made available to the application.
|
||||
///
|
||||
/// These services will be provided to the widget tree using
|
||||
/// `ArcaneServiceProvider`.
|
||||
/// If no services are specified, an empty list is used by default.
|
||||
final List<ArcaneService> services;
|
||||
|
||||
/// The root widget of the application.
|
||||
///
|
||||
/// This widget will be wrapped by the service and environment providers.
|
||||
final Widget child;
|
||||
|
||||
/// Creates an `ArcaneApp` with the specified [child] widget and optional
|
||||
/// [services].
|
||||
///
|
||||
/// The [child] is required, while the [services] list is optional. By
|
||||
/// default, the [services] list is empty.
|
||||
const ArcaneApp({
|
||||
required this.child,
|
||||
this.services = const [],
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ArcaneEnvironmentProvider(
|
||||
child: ArcaneServiceProvider(
|
||||
serviceInstances: services,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
import "package:flutter_bloc/flutter_bloc.dart";
|
||||
|
||||
/// A `Cubit` that manages the application environment state.
|
||||
///
|
||||
/// The `ArcaneEnvironment` cubit holds the current environment (`debug` or `normal`)
|
||||
/// and provides a method to enable debug mode.
|
||||
class ArcaneEnvironment extends Cubit<Environment> {
|
||||
/// Initializes the cubit with the `normal` environment as the default state.
|
||||
ArcaneEnvironment() : super(Environment.normal);
|
||||
|
||||
/// Enables debug mode by setting the environment to `Environment.debug`.
|
||||
///
|
||||
/// If provided, [onDebugModeSet] is a callback that will be awaited before switching
|
||||
/// to debug mode. This is useful for performing any setup required when enabling
|
||||
/// demo mode.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// await environmentCubit.enableDebugMode(() async {
|
||||
/// // Perform some setup when enabling demo mode.
|
||||
/// });
|
||||
/// ```
|
||||
Future<void> enableDebugMode(Future<void> Function()? onDebugModeSet) async {
|
||||
if (onDebugModeSet != null) await onDebugModeSet();
|
||||
|
||||
emit(Environment.debug);
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget that provides `ArcaneEnvironment` to the widget tree using `BlocProvider`.
|
||||
///
|
||||
/// This widget wraps around a child widget and makes `ArcaneEnvironment` available
|
||||
/// to the rest of the widget tree. It should be used in combination with `BlocProvider`
|
||||
/// from the `flutter_bloc` package.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// ArcaneEnvironmentProvider(
|
||||
/// child: MyApp(),
|
||||
/// );
|
||||
/// ```
|
||||
class ArcaneEnvironmentProvider extends StatelessWidget {
|
||||
/// The widget that will be provided with access to the `ArcaneEnvironment`.
|
||||
final Widget child;
|
||||
|
||||
/// Constructs an `ArcaneEnvironmentProvider` with the given [child].
|
||||
const ArcaneEnvironmentProvider({required this.child, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => ArcaneEnvironment(),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:collection/collection.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
|
||||
/// A provider that makes a list of `ArcaneService` instances available to the widget tree.
|
||||
///
|
||||
/// This class extends `InheritedNotifier` and allows `ArcaneService` instances to be
|
||||
/// accessed throughout the widget tree by descendant widgets. It should be used to
|
||||
/// provide service instances that are shared across the application.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// ArcaneServiceProvider(
|
||||
/// serviceInstances: [myService],
|
||||
/// child: MyApp(),
|
||||
/// );
|
||||
/// ```
|
||||
/// To access the provided services:
|
||||
/// ```dart
|
||||
/// final provider = ArcaneServiceProvider.of(context);
|
||||
/// ```
|
||||
class ArcaneServiceProvider extends InheritedNotifier {
|
||||
/// A list of `ArcaneService` instances available through the provider.
|
||||
final List<ArcaneService> serviceInstances;
|
||||
|
||||
/// Creates an `ArcaneServiceProvider` that provides [serviceInstances] to the widget tree.
|
||||
///
|
||||
/// The [child] widget will be the root of the widget subtree that has access to the services.
|
||||
@override
|
||||
const ArcaneServiceProvider({
|
||||
required this.serviceInstances,
|
||||
required super.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// Determines whether the widget should notify its dependents.
|
||||
///
|
||||
/// This always returns `true`, meaning dependents will always be notified
|
||||
/// when this widget is rebuilt.
|
||||
@override
|
||||
bool updateShouldNotify(ArcaneServiceProvider oldWidget) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Retrieves the nearest `ArcaneServiceProvider` in the widget tree.
|
||||
///
|
||||
/// This method is used to access the `ArcaneServiceProvider` and its provided services
|
||||
/// from any descendant widget. It throws an exception if no `ArcaneServiceProvider`
|
||||
/// is found in the widget tree.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final provider = ArcaneServiceProvider.of(context);
|
||||
/// ```
|
||||
static ArcaneServiceProvider of(BuildContext context) {
|
||||
final ArcaneServiceProvider? result =
|
||||
context.dependOnInheritedWidgetOfExactType<ArcaneServiceProvider>();
|
||||
|
||||
if (result == null) {
|
||||
throw Exception("ArcaneServiceProvider not found in context");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// An extension on `BuildContext` to provide easy access to `ArcaneService` instances
|
||||
/// that are registered in an `ArcaneServiceProvider`.
|
||||
///
|
||||
/// This extension provides a `serviceOfType` method, which searches for a specific
|
||||
/// service of type `T` in the current `ArcaneServiceProvider` or in the list of built-in
|
||||
/// services.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// final MyService? myService = context.serviceOfType<MyService>();
|
||||
/// ```
|
||||
extension ServiceProvider on BuildContext {
|
||||
/// Finds and returns the `ArcaneService` instance of type `T` that has been registered
|
||||
/// in the `ArcaneServiceProvider` or in the list of built-in services (`Arcane.services`).
|
||||
///
|
||||
/// If no such service is found, it returns `null`.
|
||||
///
|
||||
/// - `T`: The type of the service to be retrieved, which extends `ArcaneService`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final MyService? myService = context.serviceOfType<MyService>();
|
||||
/// ```
|
||||
T? serviceOfType<T extends ArcaneService>() {
|
||||
final T? builtInService =
|
||||
Arcane.services.firstWhereOrNull((s) => s.runtimeType == T) as T?;
|
||||
|
||||
if (builtInService != null) return builtInService;
|
||||
|
||||
final T? foundService =
|
||||
dependOnInheritedWidgetOfExactType<ArcaneServiceProvider>()
|
||||
?.serviceInstances
|
||||
.firstWhereOrNull((s) => s.runtimeType == T) as T?;
|
||||
return foundService;
|
||||
}
|
||||
}
|
||||
|
||||
/// An abstract class representing a service in the Arcane architecture.
|
||||
///
|
||||
/// Classes that extend `ArcaneService` can use `ChangeNotifier` functionality
|
||||
/// to notify listeners of changes. Services are typically registered in
|
||||
/// `ArcaneServiceProvider` and can be accessed using the `serviceOfType`
|
||||
/// method on `BuildContext`.
|
||||
abstract class ArcaneService with ChangeNotifier {}
|
||||
@@ -0,0 +1,70 @@
|
||||
part of "authentication_service.dart";
|
||||
|
||||
/// An enum representing the different steps in the sign-up process.
|
||||
///
|
||||
/// This enum has two possible values:
|
||||
/// - `confirmSignUp`: The user needs to confirm the sign-up process.
|
||||
/// - `done`: The sign-up process is complete.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// SignUpStep step = SignUpStep.confirmSignUp;
|
||||
/// if (step == SignUpStep.done) {
|
||||
/// // Sign-up process is finished
|
||||
/// }
|
||||
/// ```
|
||||
enum SignUpStep {
|
||||
/// The step where the user needs to confirm their sign-up,
|
||||
/// typically through email or other verification methods.
|
||||
confirmSignUp,
|
||||
|
||||
/// The sign-up process is complete.
|
||||
done,
|
||||
}
|
||||
|
||||
/// An enum representing the authentication status of a user.
|
||||
///
|
||||
/// This enum has three possible states:
|
||||
/// - `authenticated`: The user is authenticated.
|
||||
/// - `unauthenticated`: The user is not authenticated.
|
||||
/// - `debug`: The application is in debug mode for testing.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// AuthenticationStatus status = AuthenticationStatus.authenticated;
|
||||
/// if (status.isAuthenticated) {
|
||||
/// // User is authenticated
|
||||
/// }
|
||||
/// ```
|
||||
enum AuthenticationStatus {
|
||||
/// The user is authenticated.
|
||||
authenticated,
|
||||
|
||||
/// The user is not authenticated.
|
||||
unauthenticated,
|
||||
|
||||
/// The application is in debug mode, typically for testing or development purposes.
|
||||
debug;
|
||||
|
||||
/// Returns `true` if the current status is `debug`.
|
||||
bool get isDebug => this == debug;
|
||||
|
||||
/// Returns `true` if the current status is `authenticated`.
|
||||
bool get isAuthenticated => this == authenticated;
|
||||
|
||||
/// Returns `true` if the current status is `unauthenticated`.
|
||||
bool get isUnauthenticated => this == unauthenticated;
|
||||
}
|
||||
|
||||
/// An enum representing the different application environments.
|
||||
///
|
||||
/// This enum has two possible values:
|
||||
/// - `debug`: The application is in debug mode, typically for development and testing.
|
||||
/// - `normal`: The application is running in a normal mode, for production or standard use.
|
||||
enum Environment {
|
||||
/// The debug environment for development and testing purposes.
|
||||
debug,
|
||||
|
||||
/// The normal environment for production use.
|
||||
normal,
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
part of "authentication_service.dart";
|
||||
|
||||
/// An abstract class that defines the authentication interface.
|
||||
///
|
||||
/// This interface provides methods for various authentication operations, including
|
||||
/// signing in, signing up, resetting passwords, and managing tokens.
|
||||
abstract class ArcaneAuthInterface {
|
||||
/// Returns `true` if the user is currently signed in.
|
||||
///
|
||||
/// This is a getter that asynchronously checks if the user has an active session.
|
||||
Future<bool> get isSignedIn;
|
||||
|
||||
/// Returns the access token if available.
|
||||
///
|
||||
/// This is used to retrieve the current session's access token for authenticated
|
||||
/// API requests. Returns `null` if the user is not signed in or the token is unavailable.
|
||||
Future<String?> get accessToken;
|
||||
|
||||
/// Returns the refresh token if available.
|
||||
///
|
||||
/// The refresh token is used to renew access tokens when they expire. Returns `null` if
|
||||
/// the user is not signed in or the token is unavailable.
|
||||
Future<String?> get refreshToken;
|
||||
|
||||
/// Initializes the authentication interface.
|
||||
///
|
||||
/// This method sets up any necessary configurations or initializations required for
|
||||
/// the authentication process. It must be called before any other methods in the interface.
|
||||
Future<void> init();
|
||||
|
||||
/// Logs the user out of the session.
|
||||
///
|
||||
/// This method terminates the current session and removes any stored tokens.
|
||||
/// Returns a `Result` that either contains a `void` on success or an error message.
|
||||
Future<Result<void, String>> logout();
|
||||
|
||||
/// Logs the user in using an email address and password.
|
||||
///
|
||||
/// This method authenticates the user with their email and password credentials.
|
||||
/// Returns a `Result` that either contains a `void` on success or an error message.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// await authInterface.loginWithEmailAndPassword(
|
||||
/// email: "user@example.com",
|
||||
/// password: "password123",
|
||||
/// );
|
||||
/// ```
|
||||
Future<Result<void, String>> loginWithEmailAndPassword({
|
||||
required String email,
|
||||
required String password,
|
||||
});
|
||||
|
||||
/// Re-sends a verification code to the user's email address.
|
||||
///
|
||||
/// This method is typically used when the user hasn't received or has lost their initial
|
||||
/// verification code. Returns a `Result` that contains the verification code on success
|
||||
/// or an error message.
|
||||
Future<Result<String, String>> resendVerificationCode(
|
||||
String email,
|
||||
);
|
||||
|
||||
/// Signs a user up with a username, password, and email.
|
||||
///
|
||||
/// This method registers a new user in the system. Returns a `Result` that contains
|
||||
/// the next [SignUpStep] in the process on success or an error message.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// await authInterface.signup(
|
||||
/// email: "user@example.com",
|
||||
/// password: "password123",
|
||||
/// );
|
||||
/// ```
|
||||
Future<Result<SignUpStep, String>> signup({
|
||||
required String password,
|
||||
required String email,
|
||||
});
|
||||
|
||||
/// Confirms a user's signup using a username and a confirmation code.
|
||||
///
|
||||
/// This method completes the sign-up process by verifying the user's confirmation code.
|
||||
/// Returns a `Result` that contains `true` on success or an error message.
|
||||
Future<Result<bool, String>> confirmSignup({
|
||||
required String username,
|
||||
required String confirmationCode,
|
||||
});
|
||||
|
||||
/// Resets a user's password using an email address and a code.
|
||||
///
|
||||
/// This method is used when a user requests to reset their password. The reset code
|
||||
/// they receive via email is used to verify the request. Optionally, a new password can
|
||||
/// be provided. Returns a `Result` that contains `true` on success or an error message.
|
||||
Future<Result<bool, String>> resetPassword({
|
||||
required String email,
|
||||
String? newPassword,
|
||||
String? code,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import "dart:async";
|
||||
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
import "package:flutter_bloc/flutter_bloc.dart";
|
||||
|
||||
part "authentication_enums.dart";
|
||||
part "authentication_interface.dart";
|
||||
|
||||
/// Provides a standard interface to handle authentication-related tasks.
|
||||
///
|
||||
/// To get started, first ensure that an `ArcaneAuthInterface` has been
|
||||
/// registered.
|
||||
class ArcaneAuthenticationService extends ArcaneService {
|
||||
ArcaneAuthenticationService._internal();
|
||||
|
||||
static bool _mocked = false;
|
||||
static final ArcaneAuthenticationService _instance =
|
||||
ArcaneAuthenticationService._internal();
|
||||
|
||||
/// Provides access to the singleton instance of this service.
|
||||
static ArcaneAuthenticationService get I => _instance;
|
||||
|
||||
AuthenticationStatus _status = AuthenticationStatus.unauthenticated;
|
||||
|
||||
/// Returns the current `AuthenticationStatus`.
|
||||
///
|
||||
/// Available values:
|
||||
/// - `authenticated`: The user has successfully authenticated and is logged in.
|
||||
/// - `unauthenticated`: The user has not yet logged in.
|
||||
/// - `debug`: Debug mode has been enabled, enabling development features.
|
||||
AuthenticationStatus get status => _status;
|
||||
|
||||
static late ArcaneAuthInterface _authInterface;
|
||||
|
||||
/// Provides direct access to the registered `ArcaneAuthInterface`, if one has
|
||||
/// been registered.
|
||||
ArcaneAuthInterface get authInterface => _authInterface;
|
||||
|
||||
/// A shortcut to `status == AuthenticationStatus.authenticated`.
|
||||
bool get isAuthenticated => status == AuthenticationStatus.authenticated;
|
||||
|
||||
/// A shortcut to the `isSignedIn` getter of the registered `ArcaneAuthInterface`.
|
||||
Future<bool> get isSignedIn => authInterface.isSignedIn;
|
||||
|
||||
/// Returns a JWT access token if the registered `ArcaneAuthInterface`
|
||||
/// provides one. This token is often used in the headers of HTTP requests
|
||||
/// to the backend API.
|
||||
Future<String?> get accessToken => authInterface.accessToken;
|
||||
|
||||
/// Returns a JWT refresh token if the registered `ArcaneAuthInterface`
|
||||
/// provides one.
|
||||
Future<String?> get refreshToken => authInterface.refreshToken;
|
||||
|
||||
static ArcaneSecureStorage get _storage => Arcane.storage;
|
||||
|
||||
/// Registers an `ArcaneAuthInterface` within the `ArcaneAuthenticationService`.
|
||||
Future<void> registerInterface(ArcaneAuthInterface authInterface) async {
|
||||
_authInterface = authInterface;
|
||||
await authInterface.init();
|
||||
}
|
||||
|
||||
/// Sets `status` to `AuthenticationStatus.debug`. If `onDebugModeSet` has
|
||||
/// been specified, the method will be triggered after the new status has been
|
||||
/// set.
|
||||
Future<void> setDebug(
|
||||
BuildContext context, {
|
||||
Future<void> Function()? onDebugModeSet,
|
||||
}) async {
|
||||
if (_mocked) return;
|
||||
|
||||
ArcaneEnvironment? environment;
|
||||
|
||||
try {
|
||||
environment = context.read<ArcaneEnvironment>();
|
||||
await environment.enableDebugMode(onDebugModeSet);
|
||||
} catch (_) {
|
||||
throw Exception("No ArcaneEnvironment found in BuildContext");
|
||||
}
|
||||
|
||||
_setStatus(AuthenticationStatus.debug);
|
||||
if (onDebugModeSet != null) await onDebugModeSet();
|
||||
}
|
||||
|
||||
/// Sets `status` to `AuthenticationStatus.authenticated`.
|
||||
void setAuthenticated() {
|
||||
if (_mocked) return;
|
||||
|
||||
_setStatus(AuthenticationStatus.authenticated);
|
||||
}
|
||||
|
||||
/// Sets `status` to `AuthenticationStatus.unauthenticated`.
|
||||
void setUnauthenticated() {
|
||||
if (_mocked) return;
|
||||
|
||||
_setStatus(AuthenticationStatus.unauthenticated);
|
||||
}
|
||||
|
||||
void _setStatus(AuthenticationStatus newStatus) {
|
||||
if (_mocked) return;
|
||||
|
||||
_status = newStatus;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Logs the current user out. Upon successful logout, `status` will be set to
|
||||
/// `AuthenticationStatus.unauthenticated`.
|
||||
Future<void> logOut({required VoidCallback onLoggedOut}) async {
|
||||
if (_mocked) return;
|
||||
if (status == AuthenticationStatus.unauthenticated) return;
|
||||
|
||||
final Result<void, String> loggedOut = await authInterface.logout();
|
||||
|
||||
await loggedOut.fold(
|
||||
onSuccess: (_) async {
|
||||
await _storage.deleteAll();
|
||||
setUnauthenticated();
|
||||
onLoggedOut();
|
||||
},
|
||||
onError: (e) {
|
||||
throw Exception(e);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Attempts to log in the user using their `email` and `password`.
|
||||
/// Upon successful login, `status` will be set to
|
||||
/// `AuthenticationStatus.authenticated]` If `onLoggedIn` is specified, the
|
||||
/// method will run after the authentication status has been updated.
|
||||
/// When logging in with email and password, the user's email address will be
|
||||
/// cached in `ArcaneSecureStorage`.
|
||||
Future<Result<void, String>> loginWithEmailAndPassword({
|
||||
required String email,
|
||||
required String password,
|
||||
Future<void> Function()? onLoggedIn,
|
||||
}) async {
|
||||
if (_mocked) return Result.ok(null);
|
||||
|
||||
if (status != AuthenticationStatus.unauthenticated) {
|
||||
return Result.error("Cannot sign in. Status is already ${status.name}.");
|
||||
}
|
||||
|
||||
final Result<void, String> result =
|
||||
await authInterface.loginWithEmailAndPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
|
||||
if (result.isSuccess) {
|
||||
await _storage.setValue(ArcaneSecureStorage.emailKey, email);
|
||||
setAuthenticated();
|
||||
if (onLoggedIn != null) await onLoggedIn();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Attempts to register a new account using the provided `email` and
|
||||
/// `password`. Upon success, returns a `SignUpStep` indicating the next step
|
||||
/// in the signup process as a `SignUpStep`.
|
||||
Future<Result<SignUpStep, String>> signup({
|
||||
required String email,
|
||||
required String password,
|
||||
}) async {
|
||||
if (_mocked) return Result.ok(SignUpStep.done);
|
||||
final Result<SignUpStep, String> result = await authInterface.signup(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Confirms the user's signup using their `email` and `confirmationCode`.
|
||||
/// Returns a `Result.ok(true)` if signup was successful.
|
||||
Future<Result<bool, String>> confirmSignup({
|
||||
required String email,
|
||||
required String confirmationCode,
|
||||
}) async {
|
||||
if (_mocked) return Result.ok(false);
|
||||
final Result<bool, String> result = await authInterface.confirmSignup(
|
||||
username: email,
|
||||
confirmationCode: confirmationCode,
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Re-sends a verification code to be used when confirming the user's
|
||||
/// registration.
|
||||
Future<Result<String, String>> resendVerificationCode(String email) async {
|
||||
if (_mocked) return Result.ok("");
|
||||
return authInterface.resendVerificationCode(email);
|
||||
}
|
||||
|
||||
/// Attempts to reset the user's password using their `email`. This method
|
||||
/// should be called twice. The first call will initialize the password reset
|
||||
/// process. In the first attempt, only the `email` is provided. The second
|
||||
/// call should include the `email`, as well as a `newPassword` and
|
||||
/// `confirmationCode`. If the second call is successful, the password will be
|
||||
/// reset.
|
||||
Future<Result<bool, String>> resetPassword({
|
||||
required String email,
|
||||
String? newPassword,
|
||||
String? confirmationCode,
|
||||
}) async {
|
||||
if (_mocked) return Result.ok(false);
|
||||
final Result<bool, String> result = await authInterface.resetPassword(
|
||||
email: email,
|
||||
newPassword: newPassword,
|
||||
code: confirmationCode,
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Marks the service as mocked for testing purposes.
|
||||
///
|
||||
/// If the service is mocked, no method will be executed.
|
||||
@visibleForTesting
|
||||
static void setMocked() {
|
||||
_mocked = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
part of "feature_flags_service.dart";
|
||||
|
||||
/// An extension on `Enum` to manage feature toggles.
|
||||
///
|
||||
/// This extension provides a convenient way to enable, disable, and check the status
|
||||
/// of feature flags associated with enum values. It interacts with the `ArcaneFeatureFlags`
|
||||
/// system to manage these feature flags at runtime.
|
||||
extension FeatureToggles on Enum {
|
||||
/// Returns `true` if the feature represented by this enum is currently enabled.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// if (MyFeature.exampleFeature.enabled) {
|
||||
/// // Feature-specific logic
|
||||
/// }
|
||||
/// ```
|
||||
bool get enabled => Arcane.features.isEnabled(this);
|
||||
|
||||
/// Returns `false` if the feature represented by this enum is currently enabled.
|
||||
///
|
||||
/// This is a convenience getter that is the inverse of `enabled`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// if (MyFeature.exampleFeature.disabled) {
|
||||
/// // Logic for when the feature is disabled
|
||||
/// }
|
||||
/// ```
|
||||
bool get disabled => Arcane.features.isDisabled(this);
|
||||
|
||||
/// Enables the feature represented by this enum.
|
||||
///
|
||||
/// If the feature is already enabled, this method has no effect. It interacts with
|
||||
/// the `ArcaneFeatureFlags` system to enable the feature.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// MyFeature.exampleFeature.enable();
|
||||
/// ```
|
||||
void enable() => Arcane.features.enableFeature(this);
|
||||
|
||||
/// Disables the feature represented by this enum.
|
||||
///
|
||||
/// If the feature is already disabled, this method has no effect. It interacts with
|
||||
/// the `ArcaneFeatureFlags` system to disable the feature.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// MyFeature.exampleFeature.disable();
|
||||
/// ```
|
||||
void disable() => Arcane.features.disableFeature(this);
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
|
||||
part "feature_flags_extensions.dart";
|
||||
|
||||
/// A singleton class that manages feature flags in the Arcane architecture.
|
||||
///
|
||||
/// `ArcaneFeatureFlags` allows features to be dynamically enabled or disabled
|
||||
/// at runtime. This can be useful for controlling access to experimental or
|
||||
/// conditional functionality without requiring an application restart.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// ArcaneFeatureFlags.I.enableFeature(MyFeature.example);
|
||||
/// if (ArcaneFeatureFlags.I.isEnabled(MyFeature.example)) {
|
||||
/// // Execute feature-specific logic
|
||||
/// }
|
||||
/// ```
|
||||
class ArcaneFeatureFlags extends ArcaneService {
|
||||
ArcaneFeatureFlags._internal();
|
||||
|
||||
/// The singleton instance of `ArcaneFeatureFlags`.
|
||||
static final ArcaneFeatureFlags _instance = ArcaneFeatureFlags._internal();
|
||||
|
||||
/// Provides access to the singleton instance of `ArcaneFeatureFlags`.
|
||||
static ArcaneFeatureFlags get I => _instance;
|
||||
|
||||
/// A list of enabled features.
|
||||
///
|
||||
/// Each feature is represented as an `Enum`. The list holds the features that are
|
||||
/// currently enabled.
|
||||
final List<Enum> _enabledFeatures = [];
|
||||
List<Enum> get enabledFeatures => _enabledFeatures;
|
||||
|
||||
/// Indicates whether the feature flags have been initialized.
|
||||
bool _initialized = false;
|
||||
|
||||
/// Returns whether the feature flags have been initialized.
|
||||
///
|
||||
/// This getter is `static` and allows checking the initialization status without needing
|
||||
/// to access the instance.
|
||||
static bool get initialized => I._initialized;
|
||||
|
||||
bool _mocked = false;
|
||||
|
||||
/// Marks the feature flags as mocked for testing purposes.
|
||||
///
|
||||
/// When the feature flags are mocked, they bypass certain initializations, making
|
||||
/// them easier to work with in unit tests.
|
||||
@visibleForTesting
|
||||
void setMocked() => _mocked = true;
|
||||
|
||||
/// Checks if a specific [feature] is enabled.
|
||||
///
|
||||
/// Returns `true` if the [feature] is in the list of enabled features, otherwise returns `false`.
|
||||
bool isEnabled(Enum feature) => _enabledFeatures.contains(feature);
|
||||
|
||||
/// Checks if a specific [feature] is disabled.
|
||||
///
|
||||
/// Returns `true` if the [feature] is **not** in the list of enabled features.
|
||||
bool isDisabled(Enum feature) => !_enabledFeatures.contains(feature);
|
||||
|
||||
/// Enables a specific [feature].
|
||||
///
|
||||
/// If the [feature] is already enabled, this method does nothing. If the feature is successfully
|
||||
/// enabled, it logs the action (if the logger is initialized) and notifies listeners.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// ArcaneFeatureFlags.I.enableFeature(MyFeature.newFeature);
|
||||
/// ```
|
||||
ArcaneFeatureFlags enableFeature(Enum feature) {
|
||||
if (!I._initialized) _init();
|
||||
|
||||
if (_enabledFeatures.contains(feature)) return I;
|
||||
|
||||
_enabledFeatures.add(feature);
|
||||
|
||||
if (Arcane.logger.initialized) {
|
||||
Arcane.logger.log(
|
||||
"Feature enabled",
|
||||
level: Level.debug,
|
||||
metadata: {
|
||||
feature.name: "✅",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
return I;
|
||||
}
|
||||
|
||||
/// Disables a specific [feature].
|
||||
///
|
||||
/// If the [feature] is already disabled, this method does nothing. If the feature is successfully
|
||||
/// disabled, it logs the action (if the logger is initialized) and notifies listeners.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// ArcaneFeatureFlags.I.disableFeature(MyFeature.oldFeature);
|
||||
/// ```
|
||||
ArcaneFeatureFlags disableFeature(Enum feature) {
|
||||
if (!I._initialized) _init();
|
||||
if (!_enabledFeatures.contains(feature)) return I;
|
||||
|
||||
_enabledFeatures.remove(feature);
|
||||
|
||||
if (Arcane.logger.initialized) {
|
||||
Arcane.logger.log(
|
||||
"Feature disabled",
|
||||
level: Level.debug,
|
||||
metadata: {
|
||||
feature.name: "❌",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
return I;
|
||||
}
|
||||
|
||||
/// Initializes the feature flags.
|
||||
///
|
||||
/// This method clears the list of enabled features and marks the flags as initialized.
|
||||
/// It is called automatically when enabling or disabling features if they haven't
|
||||
/// already been initialized.
|
||||
void _init() {
|
||||
if (_mocked) return;
|
||||
|
||||
_enabledFeatures.clear();
|
||||
|
||||
I._initialized = true;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
part of "id_service.dart";
|
||||
|
||||
/// Enum representing different types of IDs managed by the `ArcaneIdService`.
|
||||
///
|
||||
/// The `ID` enum has two possible values:
|
||||
/// - `session`: Represents the session ID.
|
||||
/// - `install`: Represents the install ID.
|
||||
enum ID {
|
||||
/// Represents the session ID.
|
||||
session,
|
||||
|
||||
/// Represents the install ID.
|
||||
install,
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
import "package:uuid/uuid.dart";
|
||||
|
||||
part "id_enums.dart";
|
||||
|
||||
/// A singleton service that manages unique IDs, including install and session IDs.
|
||||
///
|
||||
/// The `ArcaneIdService` provides a way to generate and retrieve unique identifiers
|
||||
/// for application installs and sessions. It interacts with secure storage to persist
|
||||
/// the install ID across app launches and generates new session IDs for each session.
|
||||
class ArcaneIdService extends ArcaneService {
|
||||
/// Whether the service is mocked for testing purposes.
|
||||
static bool _mocked = false;
|
||||
|
||||
/// The singleton instance of `ArcaneIdService`.
|
||||
static final ArcaneIdService _instance = ArcaneIdService._internal();
|
||||
|
||||
/// Provides access to the singleton instance of `ArcaneIdService`.
|
||||
static ArcaneIdService get I => _instance;
|
||||
|
||||
ArcaneIdService._internal();
|
||||
|
||||
/// Whether the service has been initialized.
|
||||
bool _initialized = false;
|
||||
|
||||
/// Returns `true` if the service has been initialized.
|
||||
bool get initialized => I._initialized;
|
||||
|
||||
/// The unique install ID.
|
||||
///
|
||||
/// This ID is persisted across app launches and is used to uniquely identify
|
||||
/// the installation of the app.
|
||||
String? _installId;
|
||||
|
||||
/// Retrieves the install ID.
|
||||
///
|
||||
/// If the install ID is not yet initialized, this method initializes the service
|
||||
/// and generates a new ID if necessary.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// String? id = await ArcaneIdService.I.installId;
|
||||
/// ```
|
||||
Future<String?> get installId async {
|
||||
if (!initialized) await _init();
|
||||
return I._installId;
|
||||
}
|
||||
|
||||
/// The unique session ID.
|
||||
///
|
||||
/// This ID is generated for each app session and is used to uniquely identify
|
||||
/// the current session.
|
||||
String? _sessionId;
|
||||
|
||||
/// Retrieves the session ID.
|
||||
///
|
||||
/// If the session ID is not yet initialized, this method initializes the service
|
||||
/// and generates a new session ID.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// String? sessionId = await ArcaneIdService.I.sessionId;
|
||||
/// ```
|
||||
Future<String?> get sessionId async {
|
||||
if (!initialized) await _init();
|
||||
return I._sessionId;
|
||||
}
|
||||
|
||||
/// Generates a new unique ID.
|
||||
///
|
||||
/// This method uses UUID version 7 to generate a new unique ID.
|
||||
String get newId => uuid.v7();
|
||||
|
||||
/// The `Uuid` instance used for generating unique IDs.
|
||||
static const Uuid uuid = Uuid();
|
||||
|
||||
/// Initializes the `ArcaneIdService`.
|
||||
///
|
||||
/// This method retrieves the install ID from secure storage, generating and storing a new
|
||||
/// one if it does not exist. It also generates a new session ID.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// await ArcaneIdService.I._init();
|
||||
/// ```
|
||||
Future<ArcaneIdService> _init() async {
|
||||
if (_mocked) return I;
|
||||
if (!Arcane.storage.initialized) Arcane.storage.init();
|
||||
|
||||
I._installId = await Arcane.storage.getValue(
|
||||
ArcaneSecureStorage.installIdKey,
|
||||
);
|
||||
|
||||
if (I._installId == null) {
|
||||
// Generate a new ID and store it
|
||||
I._installId = uuid.v7();
|
||||
await Arcane.storage.setValue(
|
||||
ArcaneSecureStorage.installIdKey,
|
||||
I._installId,
|
||||
);
|
||||
}
|
||||
|
||||
I._sessionId = uuid.v7();
|
||||
I._initialized = true;
|
||||
return I;
|
||||
}
|
||||
|
||||
/// Sets the service as mocked for testing purposes.
|
||||
///
|
||||
/// When the service is mocked, it bypasses certain initializations and uses
|
||||
/// mocked data for testing.
|
||||
@visibleForTesting
|
||||
static void setMocked() => _mocked = true;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
part of "logging_service.dart";
|
||||
|
||||
/// Represents a logging interface that can log messages to different destinations.
|
||||
///
|
||||
/// Concrete implementations of this class should override the [log] method to provide
|
||||
/// platform-specific logging behavior.
|
||||
abstract class LoggingInterface {
|
||||
LoggingInterface._internal();
|
||||
static late final LoggingInterface _instance;
|
||||
|
||||
/// Provides access to the singleton instance of the `LoggingInterface`. This
|
||||
/// ensures that the logging interface, once configured, remains so.
|
||||
static LoggingInterface get I => _instance;
|
||||
|
||||
final bool _initialized = false;
|
||||
|
||||
/// Whether the logging interface has been initialized.
|
||||
bool get initialized => I._initialized;
|
||||
|
||||
/// Initializes the logging interface.
|
||||
///
|
||||
/// If any configuration needs to be performed on the logging interface prior
|
||||
/// to use, this is where it should be done.
|
||||
/// This method should, at a minimum, set `I._initialized = true`.
|
||||
Future<LoggingInterface?> init();
|
||||
|
||||
/// This method is called by the `ArcaneLogger` when a log message is
|
||||
/// received. See `ArcaneLogger.log` for further details on how logging
|
||||
/// works and what options are available.
|
||||
void log(
|
||||
String message, {
|
||||
Map<String, dynamic>? metadata,
|
||||
Level? level,
|
||||
StackTrace? stackTrace,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
import "dart:async";
|
||||
import "dart:io" show Platform;
|
||||
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:arcane_helper_utils/arcane_helper_utils.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
|
||||
export "package:logger/logger.dart" show Level;
|
||||
|
||||
part "logging_interface.dart";
|
||||
|
||||
/// A singleton class that manages logging to one or more logging interfaces
|
||||
/// with optional metadata.
|
||||
///
|
||||
/// The `ArcaneLogger` provides a centralized way to log messages across
|
||||
/// different parts of an application. It supports multiple logging interfaces,
|
||||
/// metadata, and platform-specific error handling. It integrates with
|
||||
/// [AppTrackingTransparency] for tracking authorization status on fruit-shaped
|
||||
/// operating systems.
|
||||
class ArcaneLogger {
|
||||
ArcaneLogger._internal();
|
||||
|
||||
static final ArcaneLogger _instance = ArcaneLogger._internal();
|
||||
|
||||
/// Provides access to the singleton instance of `ArcaneLogger`.
|
||||
static ArcaneLogger get I => _instance;
|
||||
|
||||
final List<LoggingInterface> _interfaces = [];
|
||||
|
||||
/// A list of registered logging interfaces.
|
||||
List<LoggingInterface> get interfaces => I._interfaces;
|
||||
|
||||
final Map<String, String> _additionalMetadata = {};
|
||||
|
||||
/// Additional metadata that is included in all logs.
|
||||
Map<String, String> get additionalMetadata => I._additionalMetadata;
|
||||
|
||||
TrackingStatus _trackingStatus = TrackingStatus.notDetermined;
|
||||
|
||||
/// The tracking authorization status for the current platform.
|
||||
TrackingStatus get trackingStatus => I._trackingStatus;
|
||||
|
||||
bool _initialized = false;
|
||||
|
||||
/// Whether the logger has been initialized.
|
||||
bool get initialized => I._initialized;
|
||||
|
||||
/// Marks the logger as mocked for testing purposes.
|
||||
///
|
||||
/// If the logger is mocked, platform-specific features (such as tracking
|
||||
/// status) will not be initialized.
|
||||
@visibleForTesting
|
||||
void setMocked() => _mocked = true;
|
||||
bool _mocked = false;
|
||||
|
||||
/// Initializes the logger.
|
||||
///
|
||||
/// Sets up error handling for both Flutter and platform-specific errors.
|
||||
/// Also, retrieves the tracking authorization status if running on iOS or
|
||||
/// macOS.
|
||||
Future<void> _init() async {
|
||||
if (_mocked) return;
|
||||
|
||||
additionalMetadata.clear();
|
||||
|
||||
// Handles unhandled Flutter errors by logging them.
|
||||
FlutterError.onError = (errorDetails) {
|
||||
log(
|
||||
"UNHANDLED FLUTTER ERROR",
|
||||
level: Level.error,
|
||||
module: errorDetails.library,
|
||||
stackTrace: errorDetails.stack,
|
||||
metadata: {
|
||||
"details": errorDetails.exceptionAsString(),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// Handles unhandled platform-specific errors by logging them.
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
log(
|
||||
"UNHANDLED PLATFORM ERROR",
|
||||
level: Level.error,
|
||||
stackTrace: stack,
|
||||
metadata: {
|
||||
"details": error.toString(),
|
||||
},
|
||||
);
|
||||
return true;
|
||||
};
|
||||
|
||||
I._trackingStatus =
|
||||
await AppTrackingTransparency.trackingAuthorizationStatus;
|
||||
|
||||
if (!(Platform.isIOS || Platform.isMacOS)) {
|
||||
I._trackingStatus = TrackingStatus.authorized;
|
||||
}
|
||||
|
||||
I._initialized = true;
|
||||
}
|
||||
|
||||
/// Logs a message with additional contextual information, optionally including
|
||||
/// metadata, stack trace, and log level.
|
||||
///
|
||||
/// This method provides a structured way to log messages within an application,
|
||||
/// including relevant details such as module, method, and metadata. It supports
|
||||
/// different log levels.
|
||||
///
|
||||
/// **Parameters:**
|
||||
///
|
||||
/// - `message` (String):
|
||||
/// The main log message to be recorded. This is the primary content that
|
||||
/// describes the event or state being logged.
|
||||
///
|
||||
/// - `module` (String?, _optional_):
|
||||
/// The name of the module where the log originates. If not provided, it will
|
||||
/// be inferred from the current stack trace. This helps in categorizing logs
|
||||
/// by different parts of the application.
|
||||
///
|
||||
/// - `method` (String?, _optional_):
|
||||
/// The name of the method where the log originates. If not provided, it will
|
||||
/// be inferred from the current stack trace. This adds context to the log by
|
||||
/// identifying the specific method generating the log.
|
||||
///
|
||||
/// - `level` (Level, _optional_):
|
||||
/// The severity level of the log. Defaults to `Level.debug`. This determines
|
||||
/// the importance of the log and influences how it is handled and displayed.
|
||||
///
|
||||
/// - `stackTrace` (StackTrace?, _optional_):
|
||||
/// The stack trace associated with the log event. Useful for error and
|
||||
/// warning logs to trace the execution path leading to the log event.
|
||||
///
|
||||
/// - `metadata` (Map<String, String>?, _optional_):
|
||||
/// Additional key-value pairs providing extra context for the log. Commonly
|
||||
/// used for custom information that can aid in diagnosing issues or
|
||||
/// understanding the log in context. If not provided, an empty map is used.
|
||||
///
|
||||
/// **Details:**
|
||||
///
|
||||
/// The `log` method constructs a timestamp and extracts information from the
|
||||
/// current stack trace to automatically determine the `module` and `method`
|
||||
/// if they are not explicitly provided. This process can sometimes lead to\
|
||||
/// inaccurate results, thus the optional parameters which have been provided.
|
||||
/// The metadata map is populated with default values, including `timestamp`,
|
||||
/// `module`, `method`, and `filenameAndLineNumber`.
|
||||
///
|
||||
/// The log message and associated metadata is sent to any and all registered
|
||||
/// logging interfaces.
|
||||
///
|
||||
/// **Usage:**
|
||||
///
|
||||
/// ```dart
|
||||
/// ArcaneLogger.log(
|
||||
/// "An example log message",
|
||||
/// level: Level.info,
|
||||
/// module: "MyStateManagement",
|
||||
/// method: "onProcessEvent",
|
||||
/// metadata: {
|
||||
/// "example": "value",
|
||||
/// },
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
void log(
|
||||
String message, {
|
||||
String? module,
|
||||
String? method,
|
||||
Level level = Level.debug,
|
||||
StackTrace? stackTrace,
|
||||
Map<String, String>? metadata,
|
||||
}) {
|
||||
if (I._mocked) return;
|
||||
|
||||
if (!I._initialized) {
|
||||
throw Exception("ArcaneLogger has not yet been initialized.");
|
||||
}
|
||||
|
||||
metadata ??= <String, String>{};
|
||||
|
||||
final String now = DateTime.now().toIso8601String();
|
||||
metadata.putIfAbsent("timestamp", () => now);
|
||||
|
||||
try {
|
||||
final List<String> parts = StackTrace.current
|
||||
.toString()
|
||||
.split("\n")[2]
|
||||
.split(RegExp("#2"))[1]
|
||||
.trimLeft()
|
||||
.split(".");
|
||||
|
||||
module ??= parts.first.replaceFirst("new ", "");
|
||||
method ??= parts[1].split(" ").first;
|
||||
|
||||
final String line = parts.last.substring(5).replaceAll(")", "");
|
||||
final String file = parts[1].split(" ").last.replaceAll("(package:", "");
|
||||
final String fileAndLine = "$file:$line";
|
||||
|
||||
metadata.putIfAbsent("module", () => module!);
|
||||
metadata.putIfAbsent("method", () => method!);
|
||||
metadata.putIfAbsent("filenameAndLineNumber", () => fileAndLine);
|
||||
} catch (_) {}
|
||||
|
||||
metadata.addAll(additionalMetadata);
|
||||
|
||||
// Send logs to registered interface(s)
|
||||
for (final LoggingInterface i in I._interfaces) {
|
||||
i.log(
|
||||
message,
|
||||
level: level,
|
||||
metadata: metadata,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers a [LoggingInterface] with the [ArcaneLogger]. If the current
|
||||
/// operating system is not a fruit-shaped OS, it will automatically be
|
||||
/// initalized. Otherwise, app tracking permissions must first be checked for
|
||||
/// and (optionally) granted before the interface is automatically initialized.
|
||||
///
|
||||
/// Once your [LoggingInterface] has been registered and initialized, logs
|
||||
/// will automatically be sent to the interface.
|
||||
Future<ArcaneLogger> registerInterfaces(
|
||||
List<LoggingInterface> interfaces,
|
||||
) async {
|
||||
if (!initialized) await _init();
|
||||
|
||||
for (final LoggingInterface i in interfaces) {
|
||||
I._interfaces.add(i);
|
||||
}
|
||||
|
||||
return I;
|
||||
}
|
||||
|
||||
/// Initializes all registered [LoggingInterface]s by calling their
|
||||
/// [LoggingInterface.init] methods.
|
||||
Future<ArcaneLogger> initializeInterfaces() async {
|
||||
assert(
|
||||
I._interfaces.isNotEmpty,
|
||||
"No logging interfaces have been registered.",
|
||||
);
|
||||
|
||||
if (!I._initialized) await _init();
|
||||
for (final LoggingInterface i in I._interfaces) {
|
||||
if (!i.initialized) await i.init();
|
||||
}
|
||||
|
||||
return I;
|
||||
}
|
||||
|
||||
/// This will ask the user to approve app tracking permissions on
|
||||
/// fruit-shaped operating systems. An optional `trackingDialog` method can be
|
||||
/// passed in, which could be used to display a message to users that they're
|
||||
/// about to be asked for tracking permissions. The `trackingDialog` method
|
||||
/// will only be run if the tracking status is `notDetermined`.
|
||||
///
|
||||
/// If app tracking has been allowed, all registered [LoggingInterface]s will
|
||||
/// be initialized.
|
||||
Future<void> initalizeAppTracking({
|
||||
Future<void>? trackingDialog,
|
||||
}) async {
|
||||
if (I._mocked) return;
|
||||
if (!I._initialized) await _init();
|
||||
if (I._trackingStatus == TrackingStatus.authorized) {
|
||||
await initializeInterfaces();
|
||||
return;
|
||||
}
|
||||
|
||||
// If the system can show an authorization request dialog
|
||||
if (I._trackingStatus == TrackingStatus.notDetermined) {
|
||||
// Show a custom explainer dialog before the system dialog
|
||||
if (trackingDialog != null) await trackingDialog;
|
||||
// Wait for dialog popping animation
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
// Request system's tracking authorization dialog
|
||||
await AppTrackingTransparency.requestTrackingAuthorization();
|
||||
}
|
||||
|
||||
I._trackingStatus =
|
||||
await AppTrackingTransparency.trackingAuthorizationStatus;
|
||||
|
||||
if (I._trackingStatus == TrackingStatus.authorized) {
|
||||
await initializeInterfaces();
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes a specific key from the persistent metadata.
|
||||
ArcaneLogger removePersistentMetadata(String key) {
|
||||
final bool keyPresent = additionalMetadata.containsKey(key);
|
||||
|
||||
if (keyPresent) {
|
||||
additionalMetadata.removeWhere((k, v) => k == key);
|
||||
}
|
||||
|
||||
return I;
|
||||
}
|
||||
|
||||
/// Adds or updates persistent metadata.
|
||||
///
|
||||
/// This metadata will be included in all future log messages.
|
||||
ArcaneLogger addPersistentMetadata(Map<String, String?> input) {
|
||||
for (final entry in input.entries) {
|
||||
final String key = entry.key;
|
||||
final String? value = entry.value;
|
||||
|
||||
final bool keyPresent = _additionalMetadata.containsKey(key);
|
||||
|
||||
if (keyPresent && value.isNullOrEmpty) {
|
||||
_additionalMetadata.removeWhere((k, v) => k == key);
|
||||
return I;
|
||||
}
|
||||
|
||||
if (value == null) return I;
|
||||
|
||||
_additionalMetadata.removeWhere((k, v) => k == key);
|
||||
_additionalMetadata.putIfAbsent(key, () => value);
|
||||
}
|
||||
|
||||
return I;
|
||||
}
|
||||
|
||||
/// Clears all persistent metadata.
|
||||
void clearPersistentMetadata() => _additionalMetadata.clear();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
part of "reactive_theme_service.dart";
|
||||
|
||||
/// An extension on `BuildContext` to check the current system dark mode setting.
|
||||
///
|
||||
/// This extension provides a convenient way to check whether the device is in dark mode.
|
||||
extension DarkMode on BuildContext {
|
||||
/// Returns `true` if the system is currently set to dark mode.
|
||||
///
|
||||
/// This uses `MediaQuery.of(this).platformBrightness` to check the system's brightness setting.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// if (context.isDarkMode) {
|
||||
/// // The system is in dark mode.
|
||||
/// }
|
||||
/// ```
|
||||
bool get isDarkMode {
|
||||
final brightness = MediaQuery.of(this).platformBrightness;
|
||||
return brightness == Brightness.dark;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
part "reactive_theme_extensions.dart";
|
||||
|
||||
/// A singleton service that manages theme switching and customization for the application.
|
||||
///
|
||||
/// `ArcaneReactiveTheme` allows switching between light and dark themes and provides
|
||||
/// methods to customize the themes. The current theme mode can be accessed, and the
|
||||
/// theme can be switched at runtime.
|
||||
class ArcaneReactiveTheme extends ArcaneService {
|
||||
/// The singleton instance of `ArcaneReactiveTheme`.
|
||||
static final ArcaneReactiveTheme _instance = ArcaneReactiveTheme._internal();
|
||||
|
||||
/// Provides access to the singleton instance of `ArcaneReactiveTheme`.
|
||||
static ArcaneReactiveTheme get I => _instance;
|
||||
|
||||
ArcaneReactiveTheme._internal();
|
||||
|
||||
/// Whether the current theme is dark.
|
||||
bool _isDark = false;
|
||||
|
||||
/// Returns the current theme mode based on `_isDark`.
|
||||
///
|
||||
/// If `_isDark` is true, it returns `ThemeMode.dark`, otherwise it returns `ThemeMode.light`.
|
||||
ThemeMode get currentMode => _isDark ? ThemeMode.dark : ThemeMode.light;
|
||||
|
||||
/// The `ThemeData` for the dark theme.
|
||||
ThemeData _darkTheme = ThemeData.dark();
|
||||
|
||||
/// The `ThemeData` for the light theme.
|
||||
ThemeData _lightTheme = ThemeData.light();
|
||||
|
||||
/// Returns the current dark theme `ThemeData`.
|
||||
ThemeData get dark => _darkTheme;
|
||||
|
||||
/// Returns the current light theme `ThemeData`.
|
||||
ThemeData get light => _lightTheme;
|
||||
|
||||
/// Switches the current theme between light and dark modes.
|
||||
///
|
||||
/// If the theme is currently light, it switches to dark, and vice versa. It also
|
||||
/// notifies listeners to update the UI accordingly.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// ArcaneReactiveTheme.I.switchTheme(context);
|
||||
/// ```
|
||||
ArcaneReactiveTheme switchTheme(BuildContext context) {
|
||||
_isDark = !_isDark;
|
||||
notifyListeners();
|
||||
return I;
|
||||
}
|
||||
|
||||
/// Sets a custom `ThemeData` for the dark theme.
|
||||
///
|
||||
/// This allows you to customize the dark theme and notify listeners to apply the
|
||||
/// changes immediately.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// ArcaneReactiveTheme.I.setDarkTheme(customDarkTheme);
|
||||
/// ```
|
||||
ArcaneReactiveTheme setDarkTheme(ThemeData theme) {
|
||||
_darkTheme = theme;
|
||||
notifyListeners();
|
||||
return I;
|
||||
}
|
||||
|
||||
/// Sets a custom `ThemeData` for the light theme.
|
||||
///
|
||||
/// This allows you to customize the light theme and notify listeners to apply the
|
||||
/// changes immediately.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// ArcaneReactiveTheme.I.setLightTheme(customLightTheme);
|
||||
/// ```
|
||||
ArcaneReactiveTheme setLightTheme(ThemeData theme) {
|
||||
_lightTheme = theme;
|
||||
notifyListeners();
|
||||
return I;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import "package:arcane_helper_utils/arcane_helper_utils.dart";
|
||||
import "package:flutter_secure_storage/flutter_secure_storage.dart";
|
||||
import "package:get_it/get_it.dart";
|
||||
|
||||
/// A singleton class that provides secure storage functionality using
|
||||
/// `FlutterSecureStorage`.
|
||||
///
|
||||
/// The `ArcaneSecureStorage` class is responsible for securely storing and
|
||||
/// retrieving key-value pairs, such as user email and install IDs. It supports
|
||||
/// caching for certain keys and provides initialization, deletion, and
|
||||
/// read/write methods for interacting with the secure storage.
|
||||
class ArcaneSecureStorage {
|
||||
/// The singleton instance of `ArcaneSecureStorage`.
|
||||
static final ArcaneSecureStorage _instance = ArcaneSecureStorage._internal();
|
||||
|
||||
/// Provides access to the singleton instance of `ArcaneSecureStorage`.
|
||||
static ArcaneSecureStorage get I => _instance;
|
||||
|
||||
ArcaneSecureStorage._internal();
|
||||
|
||||
/// The underlying secure storage instance.
|
||||
///
|
||||
/// This is initialized with `FlutterSecureStorage`, using encrypted shared
|
||||
/// preferences for Android.
|
||||
late final FlutterSecureStorage _storage;
|
||||
|
||||
/// Caches the user's email in memory.
|
||||
///
|
||||
/// This is used to reduce the number of reads to secure storage for the email
|
||||
/// key.
|
||||
String? _emailCache;
|
||||
|
||||
/// Provides access to the cached email if it exists.
|
||||
String? get cachedEmail => _emailCache;
|
||||
|
||||
/// The key used to store and retrieve the email from secure storage.
|
||||
static const String emailKey = "email";
|
||||
|
||||
/// The key used to store and retrieve the install ID from secure storage.
|
||||
static const String installIdKey = "installId";
|
||||
|
||||
/// Indicates whether the secure storage has been initialized.
|
||||
bool _initialized = false;
|
||||
|
||||
/// Returns `true` if the secure storage has been initialized.
|
||||
bool get initialized => I._initialized;
|
||||
|
||||
/// Initializes the secure storage and registers it with `GetIt` for
|
||||
/// dependency injection.
|
||||
///
|
||||
/// This method sets up the `FlutterSecureStorage` with encrypted shared
|
||||
/// preferences for Android, and registers it under the instance name
|
||||
/// `ArcaneSecureStorage`. It also sets the initialized flag to `true`.
|
||||
ArcaneSecureStorage init() {
|
||||
GetIt.I.registerSingleton<FlutterSecureStorage>(
|
||||
instanceName: "ArcaneSecureStorage",
|
||||
const FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(
|
||||
encryptedSharedPreferences: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
I._storage = GetIt.I<FlutterSecureStorage>(
|
||||
instanceName: "ArcaneSecureStorage",
|
||||
);
|
||||
I._initialized = true;
|
||||
return I;
|
||||
}
|
||||
|
||||
/// Deletes all key-value pairs stored in secure storage.
|
||||
///
|
||||
/// This method clears the cache and deletes all data stored in the secure\
|
||||
/// storage. It returns `true` on success and `false` on failure.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// bool success = await ArcaneSecureStorage.I.deleteAll();
|
||||
/// ```
|
||||
Future<bool> deleteAll() async {
|
||||
if (!initialized) init();
|
||||
try {
|
||||
_emailCache = null;
|
||||
await _storage.deleteAll();
|
||||
return true;
|
||||
} catch (exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves a value associated with the given [key] from secure storage.
|
||||
///
|
||||
/// If the [key] is `emailKey`, the value will be cached in memory for future
|
||||
/// use.
|
||||
/// This method returns `null` if the key is not found or if an error occurs.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// String? email = await ArcaneSecureStorage.I.getValue(ArcaneSecureStorage.emailKey);
|
||||
/// ```
|
||||
Future<String?> getValue(String key) async {
|
||||
if (!initialized) init();
|
||||
String? value;
|
||||
|
||||
try {
|
||||
value = await _storage.read(key: key);
|
||||
if (value.isNullOrEmpty) return null;
|
||||
|
||||
// Cache the email for future use
|
||||
if (key == emailKey) _emailCache = value;
|
||||
} catch (e) {
|
||||
throw Exception(e);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// Writes the given [value] associated with the [key] to secure storage.
|
||||
///
|
||||
/// This method returns `true` on success and `false` on failure.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// bool success = await ArcaneSecureStorage.I.setValue(ArcaneSecureStorage.emailKey, "user@example.com");
|
||||
/// ```
|
||||
Future<bool> setValue(String key, String? value) async {
|
||||
if (!initialized) init();
|
||||
|
||||
try {
|
||||
await _storage.write(key: key, value: value);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
name: arcane_framework
|
||||
description: "Arcane Framework is a comprehensive Dart package designed to streamline the management of essential services like logging, authentication, theming, feature flags, and secure storage in Flutter applications. With a robust architecture and easy-to-use APIs, it empowers developers to build scalable, dynamic apps with minimal overhead."
|
||||
version: 1.0.0
|
||||
repository: https://github.com/hanskokx/arcane_framework
|
||||
issue_tracker: https://github.com/hanskokx/arcane_framework/issues
|
||||
|
||||
topics:
|
||||
- arcane-framework
|
||||
|
||||
environment:
|
||||
sdk: ^3.5.2
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
app_tracking_transparency: ^2.0.6
|
||||
arcane_helper_utils: ^1.0.1
|
||||
collection: ^1.18.0
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_bloc: ^8.1.6
|
||||
flutter_secure_storage: ^9.2.2
|
||||
get_it: ^7.7.0
|
||||
logger: ^2.4.0
|
||||
result_monad: ^2.3.2
|
||||
uuid: ^4.5.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_lints: ^4.0.0
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
Reference in New Issue
Block a user