mirror of
https://github.com/hanskokx/arcane_implementations.git
synced 2026-06-09 15:16:06 +02:00
@@ -0,0 +1,127 @@
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:get_it/get_it.dart";
|
||||
import "package:uuid/uuid.dart";
|
||||
|
||||
/// A singleton service that manages unique IDs, including install and session IDs.
|
||||
///
|
||||
/// The `IdService` 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 IdService extends ArcaneService {
|
||||
/// Whether the service is mocked for testing purposes.
|
||||
static bool _mocked = false;
|
||||
|
||||
/// The singleton instance of `ArcaneIdService`.
|
||||
static final IdService _instance = IdService._internal();
|
||||
|
||||
/// Provides access to the singleton instance of `IdService`.
|
||||
static IdService get I => _instance;
|
||||
|
||||
IdService._internal();
|
||||
|
||||
SecureStorageRepository get _storage => GetIt.I<SecureStorageRepository>();
|
||||
|
||||
/// 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 = IdService.I.installId;
|
||||
/// ```
|
||||
String? get installId => 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 = ArcaneIdService.I.sessionId.value;
|
||||
/// ```
|
||||
ValueListenable<String?> get sessionId =>
|
||||
ValueNotifier<String?>(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 `IdService`.
|
||||
///
|
||||
/// 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 IdService.I._init();
|
||||
/// ```
|
||||
Future<void> init() async {
|
||||
if (_mocked) return;
|
||||
Arcane.log(
|
||||
"Initializing ID Service",
|
||||
level: Level.debug,
|
||||
);
|
||||
|
||||
I._installId = await _storage.getValue(
|
||||
SecureStorageRepository.installIdKey,
|
||||
);
|
||||
|
||||
if (I._installId == null) {
|
||||
// Generate a new ID and store it
|
||||
I._installId = uuid.v7();
|
||||
await _storage.setValue(
|
||||
SecureStorageRepository.installIdKey,
|
||||
I._installId,
|
||||
);
|
||||
}
|
||||
|
||||
I._sessionId = uuid.v7();
|
||||
I._initialized = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
||||
/// 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,90 @@
|
||||
import "dart:io";
|
||||
|
||||
import "package:app_tracking_transparency/app_tracking_transparency.dart";
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_gen/gen_l10n/app_localizations.dart";
|
||||
|
||||
class TrackingService {
|
||||
TrackingService._internal();
|
||||
|
||||
static final TrackingService _instance = TrackingService._internal();
|
||||
|
||||
static TrackingService get I => _instance;
|
||||
|
||||
TrackingStatus _trackingStatus = TrackingStatus.notDetermined;
|
||||
|
||||
TrackingStatus get trackingStatus => I._trackingStatus;
|
||||
|
||||
bool _initialized = false;
|
||||
|
||||
bool get initialized => I._initialized;
|
||||
|
||||
@visibleForTesting
|
||||
void setMocked() => _mocked = true;
|
||||
bool _mocked = false;
|
||||
|
||||
Future<void> init() async {
|
||||
if (_mocked) return;
|
||||
|
||||
_trackingStatus = await AppTrackingTransparency.trackingAuthorizationStatus;
|
||||
|
||||
if (!(Platform.isIOS || Platform.isMacOS)) {
|
||||
_trackingStatus = TrackingStatus.authorized;
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
Future<TrackingStatus?> initalizeAppTracking(BuildContext context) async {
|
||||
if (_mocked) return null;
|
||||
if (!initialized) await init();
|
||||
|
||||
// If the system can show an authorization request dialog
|
||||
if (trackingStatus == TrackingStatus.notDetermined) {
|
||||
// Show a custom explainer dialog before the system dialog
|
||||
if (!context.mounted) return null;
|
||||
await _showTrackingDialog(context);
|
||||
// Wait for dialog popping animation
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
// Request system's tracking authorization dialog
|
||||
await AppTrackingTransparency.requestTrackingAuthorization();
|
||||
}
|
||||
|
||||
_trackingStatus = await AppTrackingTransparency.trackingAuthorizationStatus;
|
||||
|
||||
if (trackingStatus == TrackingStatus.authorized) {
|
||||
await Arcane.logger.initializeInterfaces();
|
||||
}
|
||||
|
||||
return trackingStatus;
|
||||
}
|
||||
|
||||
Future<void> _showTrackingDialog(BuildContext context) async {
|
||||
await showAdaptiveDialog<void>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
final buttonStyle = Theme.of(context).textButtonTheme.style?.copyWith(
|
||||
textStyle: const WidgetStatePropertyAll(
|
||||
TextStyle(
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
);
|
||||
return AlertDialog.adaptive(
|
||||
title: Text(AppLocalizations.of(context).appTrackingTitle),
|
||||
content: Text(AppLocalizations.of(context).appTrackingBody),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: buttonStyle,
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(AppLocalizations.of(context).continueText),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:arcane_helper_utils/arcane_helper_utils.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:flutter_secure_storage/flutter_secure_storage.dart";
|
||||
|
||||
class SecureStorageRepository {
|
||||
final FlutterSecureStorage _storage;
|
||||
|
||||
SecureStorageRepository(this._storage);
|
||||
|
||||
static const String installIdKey = "install_id";
|
||||
|
||||
Future<bool> deleteAll() async {
|
||||
try {
|
||||
final String? cachedInstallId = await getValue(installIdKey);
|
||||
|
||||
await _storage.deleteAll();
|
||||
|
||||
if (cachedInstallId.isNotNullOrEmpty) {
|
||||
await setValue(installIdKey, cachedInstallId);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> getValue(String key) async {
|
||||
if (Feature.secureStorageRepositoryLogs.enabled) {
|
||||
Arcane.log(
|
||||
"Value requested from secure storage",
|
||||
level: Level.debug,
|
||||
metadata: {
|
||||
"key": key,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String? value;
|
||||
|
||||
try {
|
||||
value = await _storage.read(key: key);
|
||||
if (value.isNullOrEmpty && Feature.secureStorageRepositoryLogs.enabled) {
|
||||
Arcane.log(
|
||||
"Value retrieved from secure storage is empty",
|
||||
level: Level.info,
|
||||
metadata: {
|
||||
"key": key,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (Feature.secureStorageRepositoryLogs.enabled) {
|
||||
Arcane.log(
|
||||
"Successfully retrived value from secure storage",
|
||||
level: Level.debug,
|
||||
metadata: {
|
||||
"key": key,
|
||||
if (kDebugMode) "value": "$value",
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Arcane.log(
|
||||
"Unable to retrieve value from secure storage",
|
||||
level: Level.error,
|
||||
metadata: {
|
||||
"key": key,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
Future<bool> setValue(String key, String? value) async {
|
||||
if (Feature.secureStorageRepositoryLogs.enabled) {
|
||||
Arcane.log(
|
||||
"Setting value in secure storage",
|
||||
level: Level.debug,
|
||||
metadata: {
|
||||
"key": key,
|
||||
"value": "$value",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await _storage.write(key: key, value: value);
|
||||
|
||||
if (Feature.secureStorageRepositoryLogs.enabled) {
|
||||
Arcane.log(
|
||||
"Successfully set value in secure storage",
|
||||
level: Level.debug,
|
||||
metadata: {
|
||||
"key": key,
|
||||
if (kDebugMode) "value": "$value",
|
||||
},
|
||||
);
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
Arcane.log(
|
||||
"Unable to set value in secure storage",
|
||||
level: Level.error,
|
||||
metadata: {
|
||||
"key": key,
|
||||
if (kDebugMode) "value": "$value",
|
||||
},
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DebugSecureStorageRepository implements SecureStorageRepository {
|
||||
@override
|
||||
FlutterSecureStorage get _storage => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<bool> deleteAll() async {
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> getValue(String key) async {
|
||||
return switch (key) {
|
||||
"install_id" => "install id",
|
||||
_ => throw Exception("Unhandled case in secure storage"),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> setValue(String key, String? value) async {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user