diff --git a/.gitignore b/.gitignore index 031814c..f841c92 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,23 @@ build/ .flutter-plugins .flutter-plugins-dependencies + +# Flutter/Dart/Pub related +**/ios/Flutter/.last_build_id +**/.dart_tool/ +**/.flutter-plugins +**/.flutter-plugins-dependencies +**/.pub-cache/ +**/.pub/ +**/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +**/android/app/debug +**/android/app/profile +**/android/app/release diff --git a/CHANGELOG.md b/CHANGELOG.md index b13333a..38525c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ + +## 1.0.3+1 + +* Added example project + ## 1.0.3 * Added the ability to switch back to the normal environment from the debug environment in ArcaneEnvironment diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..29a3a50 --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,43 @@ +# 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 +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/example/.metadata b/example/.metadata new file mode 100644 index 0000000..8dda3be --- /dev/null +++ b/example/.metadata @@ -0,0 +1,30 @@ +# 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: "2663184aa79047d0a33a14a3b607954f8fdd8730" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 + base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 + - platform: web + create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 + base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..1b7a4e3 --- /dev/null +++ b/example/README.md @@ -0,0 +1,3 @@ +# example + +A new Flutter project. diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 0000000..6eade8d --- /dev/null +++ b/example/analysis_options.yaml @@ -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 diff --git a/example/lib/config.dart b/example/lib/config.dart new file mode 100644 index 0000000..2868ef5 --- /dev/null +++ b/example/lib/config.dart @@ -0,0 +1,7 @@ +enum Feature { + logging(true), + ; + + final bool enabledAtStartup; + const Feature(this.enabledAtStartup); +} diff --git a/example/lib/interfaces/debug_auth_interface.dart b/example/lib/interfaces/debug_auth_interface.dart new file mode 100644 index 0000000..4278c35 --- /dev/null +++ b/example/lib/interfaces/debug_auth_interface.dart @@ -0,0 +1,89 @@ +import "package:arcane_framework/arcane_framework.dart"; + +class DebugAuthInterface implements ArcaneAuthInterface { + DebugAuthInterface._internal(); + + static final ArcaneAuthInterface _instance = DebugAuthInterface._internal(); + static ArcaneAuthInterface get I => _instance; + + @override + Future get isSignedIn => Future.value(_isSignedIn); + bool _isSignedIn = false; + + @override + Future get accessToken => isSignedIn.then( + (loggedIn) => loggedIn ? "access_token" : null, + ); + + @override + Future get refreshToken => isSignedIn.then( + (loggedIn) => loggedIn ? "refresh_token" : null, + ); + + @override + Future> logout() async { + Arcane.log("Logging out"); + + _isSignedIn = false; + + return Result.ok(null); + } + + @override + Future> loginWithEmailAndPassword({ + required String email, + required String password, + }) async { + final bool alreadyLoggedIn = await isSignedIn; + + if (alreadyLoggedIn) return Result.ok(null); + + Arcane.log("Logging in as $email"); + + _isSignedIn = true; + + return Result.ok(null); + } + + @override + Future> resendVerificationCode(String email) async { + Arcane.log("Re-sending verification code to $email"); + return Result.ok("Code sent"); + } + + @override + Future> signup({ + required String password, + required String email, + }) async { + Arcane.log("Creating account for $email with password $password"); + return Result.ok(SignUpStep.confirmSignUp); + } + + @override + Future> confirmSignup({ + required String username, + required String confirmationCode, + }) async { + Arcane.log( + "Confirming registration for $username with code $confirmationCode", + ); + return Result.ok(true); + } + + @override + Future> resetPassword({ + required String email, + String? newPassword, + String? code, + }) async { + Arcane.log("Resetting password for $email"); + return Result.ok(true); + } + + @override + Future init() async { + Arcane.log("Debug auth interface initialized."); + return; + } +} diff --git a/example/lib/interfaces/debug_print_interface.dart b/example/lib/interfaces/debug_print_interface.dart new file mode 100644 index 0000000..b5235a2 --- /dev/null +++ b/example/lib/interfaces/debug_print_interface.dart @@ -0,0 +1,27 @@ +import "package:arcane_framework/arcane_framework.dart"; +import "package:example/config.dart"; +import "package:flutter/foundation.dart"; + +class DebugPrint implements LoggingInterface { + DebugPrint._internal(); + static final DebugPrint _instance = DebugPrint._internal(); + static DebugPrint get I => _instance; + + @override + bool get initialized => true; + + @override + void log( + String message, { + Map? metadata, + Level? level = Level.debug, + StackTrace? stackTrace, + }) { + if (Feature.logging.disabled) return; + + debugPrint("[${level!.name}] $message ($metadata)"); + } + + @override + Future init() async => I; +} diff --git a/example/lib/main.dart b/example/lib/main.dart new file mode 100644 index 0000000..ea8eced --- /dev/null +++ b/example/lib/main.dart @@ -0,0 +1,133 @@ +import "package:arcane_framework/arcane_framework.dart"; +import "package:example/config.dart"; +import "package:example/interfaces/debug_auth_interface.dart"; +import "package:example/interfaces/debug_print_interface.dart"; +import "package:example/services/demo_service.dart"; +import "package:example/theme/theme.dart"; +import "package:flutter/material.dart"; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + + for (final Feature feature in Feature.values) { + if (feature.enabledAtStartup) Arcane.features.enableFeature(feature); + } + + await Future.wait([ + Arcane.logger.registerInterfaces([ + DebugPrint.I, + ]), + IdService.I.init(), + ]); + + Arcane.logger.addPersistentMetadata({ + "session_id": IdService.I.sessionId.value, + }); + + await Arcane.auth.registerInterface(DebugAuthInterface.I); + + Arcane.theme + ..setDarkTheme(darkTheme) + ..setLightTheme(lightTheme); + + Arcane.log( + "Initialization complete.", + level: Level.info, + module: "main", + method: "main", + metadata: { + "ready": "true", + }, + ); + + runApp(const MainApp()); +} + +class MainApp extends StatefulWidget { + const MainApp({super.key}); + + @override + State createState() => _MainAppState(); +} + +class _MainAppState extends State { + @override + Widget build(BuildContext context) { + return ArcaneApp( + services: [ + IdService.I, + ], + child: MaterialApp( + debugShowCheckedModeBanner: false, + theme: Arcane.theme.light, + darkTheme: Arcane.theme.dark, + themeMode: Arcane.theme.currentMode, + home: Scaffold( + appBar: AppBar( + title: const Text("Arcane Framework Example"), + actions: [ + IconButton( + icon: const Icon(Icons.contrast), + onPressed: () { + Arcane.theme.switchTheme(); + setState(() {}); + }, + ), + ], + ), + body: const HomeScreen(), + ), + ), + ); + } +} + +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + @override + Widget build(BuildContext context) { + final bool isSignedIn = Arcane.auth.isSignedIn.value; + return Center( + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Authentication status: ${Arcane.auth.status.name}", + ), + if (isSignedIn) + ElevatedButton( + child: const Text("Sign out"), + onPressed: () async { + await Arcane.auth.logOut( + onLoggedOut: () async { + setState(() {}); + }, + ); + }, + ), + if (!isSignedIn) + ElevatedButton( + child: const Text("Sign in"), + onPressed: () async { + await Arcane.auth.loginWithEmailAndPassword( + email: "email", + password: "password", + onLoggedIn: () async { + setState(() {}); + }, + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/services/demo_service.dart b/example/lib/services/demo_service.dart new file mode 100644 index 0000000..d4f7f3e --- /dev/null +++ b/example/lib/services/demo_service.dart @@ -0,0 +1,33 @@ +import "package:arcane_framework/arcane_framework.dart"; +import "package:flutter/foundation.dart"; +import "package:uuid/uuid.dart"; + +class IdService extends ArcaneService { + static final IdService _instance = IdService._internal(); + static IdService get I => _instance; + + IdService._internal(); + + bool _initialized = false; + bool get initialized => I._initialized; + + String? _sessionId; + ValueListenable get sessionId => + ValueNotifier(I._sessionId); + + String get newId => uuid.v7(); + + /// The `Uuid` instance used for generating unique IDs. + static const Uuid uuid = Uuid(); + + Future init() async { + Arcane.log( + "Initializing ID Service", + level: Level.debug, + ); + + I._sessionId = uuid.v7(); + I._initialized = true; + notifyListeners(); + } +} diff --git a/example/lib/theme/theme.dart b/example/lib/theme/theme.dart new file mode 100644 index 0000000..0491b2b --- /dev/null +++ b/example/lib/theme/theme.dart @@ -0,0 +1,4 @@ +import "package:flutter/material.dart"; + +final ThemeData darkTheme = ThemeData.dark(); +final ThemeData lightTheme = ThemeData.light(); diff --git a/example/pubspec.yaml b/example/pubspec.yaml new file mode 100644 index 0000000..dc8ea71 --- /dev/null +++ b/example/pubspec.yaml @@ -0,0 +1,22 @@ +name: example +description: "A new Flutter project." +publish_to: "none" +version: 0.1.0 + +environment: + sdk: ^3.5.3 + +dependencies: + arcane_framework: + path: "../" + flutter: + sdk: flutter + uuid: ^4.5.0 + +dev_dependencies: + flutter_lints: ^4.0.0 + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/lib/src/services/reactive_theme/reactive_theme_service.dart b/lib/src/services/reactive_theme/reactive_theme_service.dart index 6424e37..cd9553c 100644 --- a/lib/src/services/reactive_theme/reactive_theme_service.dart +++ b/lib/src/services/reactive_theme/reactive_theme_service.dart @@ -38,6 +38,7 @@ class ArcaneReactiveTheme extends ArcaneService { /// Returns the current light theme `ThemeData`. ThemeData get light => _lightTheme; + /// A listenable that notifies listeners when the syste theme mode changes. ValueListenable get systemTheme => ValueNotifier(_isDark ? ThemeMode.dark : ThemeMode.light); diff --git a/pubspec.yaml b/pubspec.yaml index 2da903f..ef6e83a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: arcane_framework -description: "Agnostic Reusable Component Architecture for New Ecosystems" -version: 1.0.3 +description: "Agnostic Reusable Component Architecture for New Ecosystems: a modern framework for bootstrapping new applications" +version: 1.0.3+1 repository: https://github.com/hanskokx/arcane_framework issue_tracker: https://github.com/hanskokx/arcane_framework/issues