mirror of
https://github.com/hanskokx/arcane_framework.git
synced 2026-05-14 10:29:06 +02:00
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user