diff --git a/android/app/src/main/kotlin/io/ion/app/MainActivity.kt b/android/app/src/main/kotlin/io/ion/app/MainActivity.kt index 03717d17c..6d1f8fe9f 100644 --- a/android/app/src/main/kotlin/io/ion/app/MainActivity.kt +++ b/android/app/src/main/kotlin/io/ion/app/MainActivity.kt @@ -3,15 +3,27 @@ package io.ion.app import android.content.Intent import android.net.Uri import android.os.Bundle -import com.banuba.sdk.pe.PhotoCreationActivity +import android.view.KeyEvent import com.banuba.sdk.pe.BanubaPhotoEditor +import com.banuba.sdk.pe.PhotoCreationActivity import com.banuba.sdk.pe.data.PhotoEditorConfig +import dev.fluttercommunity.shake_gesture_android.ShakeGesturePlugin import io.flutter.embedding.android.FlutterActivity import io.flutter.plugin.common.MethodChannel import io.flutter.plugins.GeneratedPluginRegistrant import java.io.File class MainActivity : FlutterActivity() { + override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { + if (keyCode == KeyEvent.KEYCODE_MENU) { + this.flutterEngine?.plugins?.get(ShakeGesturePlugin::class.java).let { plugin -> + if (plugin is ShakeGesturePlugin) + plugin.onShake() + } + } + + return super.onKeyDown(keyCode, event) + } companion object { // For Photo Editor diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 0fe719dcf..4d85be630 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -161,6 +161,8 @@ PODS: - SDWebImage (5.20.0): - SDWebImage/Core (= 5.20.0) - SDWebImage/Core (5.20.0) + - shake_gesture_ios (0.0.1): + - Flutter - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -241,6 +243,7 @@ DEPENDENCIES: - photo_manager (from `.symlinks/plugins/photo_manager/ios`) - qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`) - quill_native_bridge (from `.symlinks/plugins/quill_native_bridge/ios`) + - shake_gesture_ios (from `.symlinks/plugins/shake_gesture_ios/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite (from `.symlinks/plugins/sqflite/darwin`) @@ -333,6 +336,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/qr_code_scanner/ios" quill_native_bridge: :path: ".symlinks/plugins/quill_native_bridge/ios" + shake_gesture_ios: + :path: ".symlinks/plugins/shake_gesture_ios/ios" share_plus: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: @@ -400,9 +405,10 @@ SPEC CHECKSUMS: qr_code_scanner: d77f94ecc9abf96d9b9b8fc04ef13f611e5a147a quill_native_bridge: fd2819cf6da02fb6cbf9de37835f96e798e145eb SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8 - share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 - sqflite: c35dad70033b8862124f8337cc994a809fcd9fa3 + shake_gesture_ios: 64f1f579f314c58445761992a123111b3d7b3492 + share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec sqlite3: 7559e33dae4c78538df563795af3a86fc887ee71 sqlite3_flutter_libs: f0b59f6bb2a18597d0796558725007e5a7428397 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 diff --git a/lib/app/features/auth/views/pages/discover_creators/discover_creators.dart b/lib/app/features/auth/views/pages/discover_creators/discover_creators.dart index 4ca89a1f3..5473e84f5 100644 --- a/lib/app/features/auth/views/pages/discover_creators/discover_creators.dart +++ b/lib/app/features/auth/views/pages/discover_creators/discover_creators.dart @@ -37,7 +37,7 @@ class DiscoverCreators extends HookConsumerWidget { final contentCreators = entitiesPagedData?.data.items?.whereType(); final hideCreatorsWithoutPicture = ref - .watch(featureFlagsProvider.notifier) + .read(featureFlagsProvider.notifier) .get(HideCreatorsWithoutPicture.hideCreatorsWithoutPicture); final filteredCreators = hideCreatorsWithoutPicture diff --git a/lib/app/features/config/providers/force_update_provider.c.dart b/lib/app/features/config/providers/force_update_provider.c.dart index ee476bcbf..541b4a71d 100644 --- a/lib/app/features/config/providers/force_update_provider.c.dart +++ b/lib/app/features/config/providers/force_update_provider.c.dart @@ -42,8 +42,6 @@ class ForceUpdate extends _$ForceUpdate { } Future _checkAndUpdateConfig() async { - await ref.read(envProvider.future); - final refetchIntervalInMilliseconds = ref.read(envProvider.notifier).get(EnvVariable.VERSIONS_CONFIG_REFETCH_INTERVAL); diff --git a/lib/app/features/core/model/feature_flags.dart b/lib/app/features/core/model/feature_flags.dart index a0cef6dff..1aedfe66d 100644 --- a/lib/app/features/core/model/feature_flags.dart +++ b/lib/app/features/core/model/feature_flags.dart @@ -19,6 +19,15 @@ final class FeedFeatureFlag extends FeatureFlag { static const showMentionsSuggestions = FeedFeatureFlag._(key: 'showMentionsSuggestions'); } +final class LoggerFeatureFlag extends FeatureFlag { + const LoggerFeatureFlag._({required super.key}); + + static const logApp = FeedFeatureFlag._(key: 'logApp'); + static const logRouters = FeedFeatureFlag._(key: 'logRouters'); + static const logNostrDart = FeedFeatureFlag._(key: 'logNostrDart'); + static const logIonIdentityClient = FeedFeatureFlag._(key: 'logIonIdentityClient'); +} + /// /// TODO: remove this once before production release /// It hides creators without picture from the discover creators page diff --git a/lib/app/features/core/providers/dio_provider.c.dart b/lib/app/features/core/providers/dio_provider.c.dart index 215bdb187..583bda8db 100644 --- a/lib/app/features/core/providers/dio_provider.c.dart +++ b/lib/app/features/core/providers/dio_provider.c.dart @@ -2,8 +2,7 @@ import 'package:dio/dio.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:ion/app/services/logger/config.dart'; -import 'package:pretty_dio_logger/pretty_dio_logger.dart'; +import 'package:ion/app/services/logger/logger.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'dio_provider.c.g.dart'; @@ -11,8 +10,12 @@ part 'dio_provider.c.g.dart'; @riverpod Dio dio(Ref ref) { final dio = Dio(); - if (LoggerConfig.dioLogsEnabled) { - dio.interceptors.add(PrettyDioLogger()); + + final logger = Logger.talkerDioLogger; + + if (logger != null) { + dio.interceptors.add(logger); } + return dio; } diff --git a/lib/app/features/core/providers/env_provider.c.dart b/lib/app/features/core/providers/env_provider.c.dart index f29249bbf..e086d15f8 100644 --- a/lib/app/features/core/providers/env_provider.c.dart +++ b/lib/app/features/core/providers/env_provider.c.dart @@ -3,7 +3,6 @@ // ignore_for_file: constant_identifier_names import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:ion/generated/assets.gen.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'env_provider.c.g.dart'; @@ -21,35 +20,19 @@ enum EnvVariable { @Riverpod(keepAlive: true) class Env extends _$Env { @override - Future build() async { - await dotenv.load(fileName: Assets.aApp); - final notDefined = _getNotDefined(); - if (notDefined.isNotEmpty) { - throw Exception('Invalid ENV value for $notDefined'); - } - } - - List _getNotDefined() { - return EnvVariable.values - .where((EnvVariable element) => dotenv.maybeGet(element.name) == null) - .toList(); - } + void build() {} + /// Gets a typed environment variable value. + /// Throws if the variable is not found or cannot be converted to type [T]. T get(EnvVariable variable) { final value = dotenv.get(variable.name); - if (T == bool) { - return (value.toLowerCase() == 'true') as T; - } - - if (T == int) { - return int.parse(value) as T; - } - - if (T == double) { - return double.parse(value) as T; - } - - return value as T; + return switch (T) { + bool => (value.toLowerCase() == 'true') as T, + int => int.parse(value) as T, + double => double.parse(value) as T, + String => value as T, + _ => throw Exception('Unsupported type $T'), + }; } } diff --git a/lib/app/features/core/providers/feature_flags_provider.c.dart b/lib/app/features/core/providers/feature_flags_provider.c.dart index 6a61d9cd7..56ae4a835 100644 --- a/lib/app/features/core/providers/feature_flags_provider.c.dart +++ b/lib/app/features/core/providers/feature_flags_provider.c.dart @@ -1,64 +1,31 @@ // SPDX-License-Identifier: ice License 1.0 import 'package:ion/app/features/core/model/feature_flags.dart'; -import 'package:ion/app/services/logger/logger.dart'; +import 'package:ion/app/features/core/providers/env_provider.c.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'feature_flags_provider.c.g.dart'; -abstract class FeatureFlagsService { - const FeatureFlagsService(); - - bool? get(FeatureFlag flag); - - Set get supportedFlags; -} - -final class LocalFeatureFlagsService extends FeatureFlagsService { - factory LocalFeatureFlagsService() { - return const LocalFeatureFlagsService._({ +@Riverpod(keepAlive: true) +class FeatureFlags extends _$FeatureFlags { + @override + Map build() { + return { + /// Local flags WalletFeatureFlag.buyNftEnabled: false, FeedFeatureFlag.showTrendingVideo: false, FeedFeatureFlag.showMentionsSuggestions: false, HideCreatorsWithoutPicture.hideCreatorsWithoutPicture: true, - }); - } - - const LocalFeatureFlagsService._(this._featuresMap); - - final Map _featuresMap; - - @override - bool? get(FeatureFlag flag) => _featuresMap[flag]; - @override - Set get supportedFlags => _featuresMap.keys.toSet(); -} - -@Riverpod(keepAlive: true) -class FeatureFlags extends _$FeatureFlags { - late final Set _services; - - @override - Future build() async { - _services = { - LocalFeatureFlagsService(), + /// Log flags + if (ref.watch(envProvider.notifier).get(EnvVariable.SHOW_DEBUG_INFO)) ...{ + LoggerFeatureFlag.logApp: true, + LoggerFeatureFlag.logRouters: false, + LoggerFeatureFlag.logNostrDart: true, + LoggerFeatureFlag.logIonIdentityClient: true, + }, }; } - bool get(FeatureFlag flag, {bool defaultValue = false}) { - for (final service in _services) { - if (service.supportedFlags.contains(flag)) { - final value = service.get(flag); - - if (value != null) { - return value; - } - } - } - - Logger.log('${flag.key} not found.'); - - return defaultValue; - } + bool get(FeatureFlag flag) => state[flag] ?? false; } diff --git a/lib/app/features/core/providers/init_provider.c.dart b/lib/app/features/core/providers/init_provider.c.dart index 3b3eaa256..87d1f24c3 100644 --- a/lib/app/features/core/providers/init_provider.c.dart +++ b/lib/app/features/core/providers/init_provider.c.dart @@ -3,12 +3,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:ion/app/features/auth/providers/auth_provider.c.dart'; import 'package:ion/app/features/auth/providers/onboarding_complete_provider.c.dart'; +import 'package:ion/app/features/core/model/feature_flags.dart'; import 'package:ion/app/features/core/permissions/providers/permissions_provider.c.dart'; -import 'package:ion/app/features/core/providers/env_provider.c.dart'; +import 'package:ion/app/features/core/providers/feature_flags_provider.c.dart'; import 'package:ion/app/features/core/providers/template_provider.c.dart'; import 'package:ion/app/features/core/providers/window_manager_provider.c.dart'; import 'package:ion/app/features/wallet/data/coins/domain/coin_initializer.c.dart'; +import 'package:ion/app/services/logger/logger.dart'; import 'package:ion/app/services/nostr/nostr.dart'; +import 'package:ion/app/services/nostr/nostr_logger.dart'; import 'package:ion/app/services/storage/local_storage.c.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -16,20 +19,21 @@ part 'init_provider.c.g.dart'; @Riverpod(keepAlive: true) Future initApp(Ref ref) async { - Nostr.initialize(); + final featureFlagsNotifier = ref.read(featureFlagsProvider.notifier); + final logApp = featureFlagsNotifier.get(LoggerFeatureFlag.logApp); + final logNostrDart = featureFlagsNotifier.get(LoggerFeatureFlag.logNostrDart); + + if (logApp) Logger.init(); + + Nostr.initialize(logNostrDart ? NostrLogger() : null); await Future.wait([ ref.read(windowManagerProvider.notifier).show(), - ref.read(envProvider.future), ref.read(sharedPreferencesProvider.future), - ]); - - await Future.wait([ ref.read(appTemplateProvider.future), ref.read(authProvider.future), ref.read(permissionsProvider.notifier).checkAllPermissions(), ref.read(coinInitializerProvider).initialize(), + ref.read(onboardingCompleteProvider.future), ]); - - await ref.read(onboardingCompleteProvider.future); } diff --git a/lib/app/features/core/views/pages/error_modal.dart b/lib/app/features/core/views/pages/error_modal.dart index c398a5cbf..66bff2f49 100644 --- a/lib/app/features/core/views/pages/error_modal.dart +++ b/lib/app/features/core/views/pages/error_modal.dart @@ -10,10 +10,13 @@ import 'package:ion/app/components/screen_offset/screen_side_offset.dart'; import 'package:ion/app/exceptions/exceptions.dart'; import 'package:ion/app/extensions/extensions.dart'; import 'package:ion/app/features/core/providers/env_provider.c.dart'; +import 'package:ion/app/services/logger/logger.dart'; import 'package:ion/generated/assets.gen.dart'; class ErrorModal extends ConsumerWidget { - const ErrorModal({required this.error, super.key}); + ErrorModal({required this.error, super.key}) { + Logger.error(error); + } final Object error; diff --git a/lib/app/features/debug/views/debug_page.dart b/lib/app/features/debug/views/debug_page.dart new file mode 100644 index 000000000..ab259ff8c --- /dev/null +++ b/lib/app/features/debug/views/debug_page.dart @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: ice License 1.0 + +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:ion/app/extensions/extensions.dart'; +import 'package:ion/app/features/core/providers/feature_flags_provider.c.dart'; +import 'package:ion/app/router/components/navigation_app_bar/navigation_app_bar.dart'; +import 'package:ion/app/router/components/navigation_app_bar/navigation_close_button.dart'; +import 'package:ion/app/services/logger/logger.dart'; +import 'package:talker_flutter/talker_flutter.dart'; + +class DebugPage extends ConsumerWidget { + const DebugPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final featureFlags = ref.watch(featureFlagsProvider); + final talker = Logger.talker; + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + NavigationAppBar.modal( + title: const Text('🐞 Debug'), + showBackButton: false, + actions: const [NavigationCloseButton()], + ), + Flexible( + child: Padding( + padding: const EdgeInsets.all(16), + child: ListView( + shrinkWrap: true, + children: [ + if (talker != null) + Card( + child: ListTile( + leading: const Icon(Icons.bug_report), + title: const Text('View Debug Logs'), + subtitle: const Text('Check application logs and diagnostics'), + trailing: const Icon(Icons.chevron_right), + onTap: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => TalkerScreen( + talker: talker, + ), + ), + ), + ), + ), + SizedBox(height: 16.0.s), + ExpansionTile( + title: const Text('Feature Flags'), + children: featureFlags.entries + .map( + (entry) => Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: ListTile( + title: Text(entry.key.key), + trailing: Icon( + entry.value ? Icons.check_circle : Icons.cancel, + color: entry.value ? Colors.green : Colors.red, + ), + ), + ), + ) + .toList(), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/app/features/debug/views/debug_shake_gesture.dart b/lib/app/features/debug/views/debug_shake_gesture.dart new file mode 100644 index 000000000..b09ad0e9b --- /dev/null +++ b/lib/app/features/debug/views/debug_shake_gesture.dart @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: ice License 1.0 + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:ion/app/features/core/providers/env_provider.c.dart'; +import 'package:ion/app/features/debug/views/debug_page.dart'; +import 'package:ion/app/router/utils/show_simple_bottom_sheet.dart'; +import 'package:shake_gesture/shake_gesture.dart'; + +class DebugShakeGesture extends HookConsumerWidget { + const DebugShakeGesture({ + required this.child, + super.key, + }); + + final Widget child; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final showDebugInfo = ref.watch(envProvider.notifier).get(EnvVariable.SHOW_DEBUG_INFO); + + if (!showDebugInfo) return child; + + final isSheetOpen = useState(false); + + return Navigator( + onGenerateRoute: (settings) => MaterialPageRoute( + builder: (context) => ShakeGesture( + onShake: () { + if (isSheetOpen.value) return; + + isSheetOpen.value = true; + showSimpleBottomSheet( + context: context, + child: const DebugPage(), + onPopInvokedWithResult: (didPop, _) { + if (didPop) { + isSheetOpen.value = false; + } + }, + ); + }, + child: child, + ), + ), + ); + } +} diff --git a/lib/app/features/feed/views/pages/feed_page/feed_page.dart b/lib/app/features/feed/views/pages/feed_page/feed_page.dart index dc825f62c..5b9bf01c1 100644 --- a/lib/app/features/feed/views/pages/feed_page/feed_page.dart +++ b/lib/app/features/feed/views/pages/feed_page/feed_page.dart @@ -35,7 +35,7 @@ class FeedPage extends HookConsumerWidget { .select((state) => (state?.hasMore).falseOrValue), ); final showTrendingVideos = - ref.read(featureFlagsProvider.notifier).get(FeedFeatureFlag.showTrendingVideo); + ref.watch(featureFlagsProvider.notifier).get(FeedFeatureFlag.showTrendingVideo); useScrollTopOnTabPress(context, scrollController: scrollController); diff --git a/lib/app/router/components/app_router_builder.dart b/lib/app/router/components/app_router_builder.dart index f2324016e..e2d42834c 100644 --- a/lib/app/router/components/app_router_builder.dart +++ b/lib/app/router/components/app_router_builder.dart @@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:ion/app/components/global_notification_bar/global_notification_bar.dart'; import 'package:ion/app/components/global_notification_bar/providers/global_notification_provider.c.dart'; import 'package:ion/app/extensions/extensions.dart'; +import 'package:ion/app/features/debug/views/debug_shake_gesture.dart'; import 'package:ion/app/hooks/use_interval.dart'; import 'package:ion/app/services/ui_event_queue/ui_event_queue_listener.dart'; @@ -41,7 +42,9 @@ class AppRouterBuilder extends HookConsumerWidget { const GlobalNotificationBar(), const UiEventQueueListener(), Expanded( - child: child ?? const SizedBox.shrink(), + child: DebugShakeGesture( + child: child ?? const SizedBox.shrink(), + ), ), ], ), diff --git a/lib/app/router/providers/go_router_provider.c.dart b/lib/app/router/providers/go_router_provider.c.dart index c2f4286da..8cec5edf8 100644 --- a/lib/app/router/providers/go_router_provider.c.dart +++ b/lib/app/router/providers/go_router_provider.c.dart @@ -6,15 +6,16 @@ import 'package:ion/app/extensions/extensions.dart'; import 'package:ion/app/features/auth/providers/auth_provider.c.dart'; import 'package:ion/app/features/auth/providers/onboarding_complete_provider.c.dart'; import 'package:ion/app/features/auth/views/pages/link_new_device/link_new_device_dialog.dart'; +import 'package:ion/app/features/core/model/feature_flags.dart'; import 'package:ion/app/features/core/permissions/data/models/permissions_types.dart'; import 'package:ion/app/features/core/permissions/providers/permissions_provider.c.dart'; +import 'package:ion/app/features/core/providers/feature_flags_provider.c.dart'; import 'package:ion/app/features/core/providers/init_provider.c.dart'; import 'package:ion/app/features/core/providers/splash_provider.c.dart'; import 'package:ion/app/features/core/views/pages/error_page.dart'; import 'package:ion/app/features/user/providers/user_metadata_provider.c.dart'; import 'package:ion/app/router/app_router_listenable.dart'; import 'package:ion/app/router/app_routes.c.dart'; -import 'package:ion/app/services/logger/config.dart'; import 'package:ion/app/services/logger/logger.dart'; import 'package:ion/app/services/ui_event_queue/ui_event_queue_notifier.c.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -49,7 +50,7 @@ GoRouter goRouter(Ref ref) { routes: $appRoutes, errorBuilder: (context, state) => ErrorPage(error: state.error ?? Exception('Unknown error')), initialLocation: SplashRoute().location, - debugLogDiagnostics: LoggerConfig.routerLogsEnabled, + debugLogDiagnostics: ref.read(featureFlagsProvider.notifier).get(LoggerFeatureFlag.logRouters), navigatorKey: rootNavigatorKey, ); } diff --git a/lib/app/router/utils/show_simple_bottom_sheet.dart b/lib/app/router/utils/show_simple_bottom_sheet.dart index f2ac39432..68466424b 100644 --- a/lib/app/router/utils/show_simple_bottom_sheet.dart +++ b/lib/app/router/utils/show_simple_bottom_sheet.dart @@ -9,6 +9,7 @@ Future showSimpleBottomSheet({ required Widget child, bool useRootNavigator = true, bool isDismissible = true, + PopInvokedWithResultCallback? onPopInvokedWithResult, }) { return showModalBottomSheet( useRootNavigator: useRootNavigator, @@ -20,6 +21,7 @@ Future showSimpleBottomSheet({ builder: (context) { return PopScope( canPop: isDismissible, + onPopInvokedWithResult: onPopInvokedWithResult, child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.end, diff --git a/lib/app/services/ion_identity/ion_identity_provider.c.dart b/lib/app/services/ion_identity/ion_identity_provider.c.dart index 9f0e4f53d..aa2074fc0 100644 --- a/lib/app/services/ion_identity/ion_identity_provider.c.dart +++ b/lib/app/services/ion_identity/ion_identity_provider.c.dart @@ -3,8 +3,10 @@ import 'dart:io'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:ion/app/features/core/model/feature_flags.dart'; import 'package:ion/app/features/core/providers/env_provider.c.dart'; -import 'package:ion/app/services/logger/config.dart'; +import 'package:ion/app/features/core/providers/feature_flags_provider.c.dart'; +import 'package:ion/app/services/logger/logger.dart'; import 'package:ion_identity_client/ion_identity.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -12,17 +14,19 @@ part 'ion_identity_provider.c.g.dart'; @Riverpod(keepAlive: true) Future> ionIdentity(Ref ref) async { - await ref.watch(envProvider.future); - final envController = ref.watch(envProvider.notifier); + final env = ref.watch(envProvider.notifier); - final appId = envController.get( + final appId = env.get( Platform.isAndroid ? EnvVariable.ION_ANDROID_APP_ID : EnvVariable.ION_IOS_APP_ID, ); + final logIonIdentityClient = + ref.read(featureFlagsProvider.notifier).get(LoggerFeatureFlag.logIonIdentityClient); + final config = IONIdentityConfig( appId: appId, - origin: envController.get(EnvVariable.ION_ORIGIN), - logging: LoggerConfig.ionIdentityLogsEnabled, + origin: env.get(EnvVariable.ION_ORIGIN), + logger: logIonIdentityClient ? Logger.talkerDioLogger : null, ); final ionClient = IONIdentity.createDefault(config: config); diff --git a/lib/app/services/logger/config.dart b/lib/app/services/logger/config.dart deleted file mode 100644 index 596d31536..000000000 --- a/lib/app/services/logger/config.dart +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: ice License 1.0 - -class LoggerConfig { - LoggerConfig._(); - - static const bool riverpodLogsEnabled = false; - - static const bool routerLogsEnabled = true; - - static const bool nostrLogsEnabled = true; - - static const bool ionIdentityLogsEnabled = true; - - static const bool dioLogsEnabled = true; -} diff --git a/lib/app/services/logger/logger.dart b/lib/app/services/logger/logger.dart index e5b1fbc2d..165125264 100644 --- a/lib/app/services/logger/logger.dart +++ b/lib/app/services/logger/logger.dart @@ -1,16 +1,55 @@ // SPDX-License-Identifier: ice License 1.0 -import 'dart:developer' as developer; +import 'package:talker_dio_logger/talker_dio_logger_interceptor.dart'; +import 'package:talker_dio_logger/talker_dio_logger_settings.dart'; +import 'package:talker_flutter/talker_flutter.dart'; class Logger { Logger._(); + static Talker? _talker; + + static void init() { + _talker = TalkerFlutter.init(); + } + + static Talker? get talker => _talker; + + static TalkerDioLogger? get talkerDioLogger => TalkerDioLogger( + talker: talker, + settings: TalkerDioLoggerSettings( + printRequestHeaders: true, + printResponseHeaders: true, + requestPen: AnsiPen()..cyan(), + responsePen: AnsiPen()..green(), + errorPen: AnsiPen()..red(), + ), + ); + static void log( String message, { - String name = '', Object? error, StackTrace? stackTrace, }) { - developer.log(message, name: name, error: error, stackTrace: stackTrace); + _talker?.log(message); + + if (error != null) { + _talker?.error(error, stackTrace); + } + } + + static void info(String message) { + _talker?.info(message); + } + + static void warning(String message) { + _talker?.warning(message); + } + + static void error( + Object error, { + StackTrace? stackTrace, + }) { + _talker?.error(error, stackTrace); } } diff --git a/lib/app/services/nostr/nostr.dart b/lib/app/services/nostr/nostr.dart index ebc4df7f1..55c464427 100644 --- a/lib/app/services/nostr/nostr.dart +++ b/lib/app/services/nostr/nostr.dart @@ -1,17 +1,16 @@ // SPDX-License-Identifier: ice License 1.0 -import 'package:ion/app/services/logger/config.dart'; +import 'package:ion/app/services/nostr/nostr_logger.dart'; import 'package:ion/app/services/nostr/nostr_signature_verifier.dart'; import 'package:nostr_dart/nostr_dart.dart'; class Nostr { Nostr._(); - static void initialize() { + static void initialize(NostrLogger? logger) { NostrDart.configure( - // ignore: avoid_redundant_argument_values - logLevel: LoggerConfig.nostrLogsEnabled ? NostrLogLevel.ALL : NostrLogLevel.OFF, signatureVerifier: NostrSignatureVerifier(), + logger: logger, ); } } diff --git a/lib/app/services/nostr/nostr_logger.dart b/lib/app/services/nostr/nostr_logger.dart new file mode 100644 index 000000000..d17f2acc0 --- /dev/null +++ b/lib/app/services/nostr/nostr_logger.dart @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: ice License 1.0 + +import 'package:ion/app/services/logger/logger.dart'; +import 'package:nostr_dart/nostr_dart.dart'; + +class NostrLogger implements NostrDartLogger { + static const _prefix = '🦩 Nostr:'; + + @override + void info(String message, [Object? error, StackTrace? stackTrace]) { + Logger.info('$_prefix $message'); + + if (error != null) { + Logger.error('$_prefix $error', stackTrace: stackTrace); + } + } + + @override + void warning(String message, [Object? error, StackTrace? stackTrace]) { + Logger.warning('$_prefix $message'); + + if (error != null) { + Logger.error('$_prefix $error', stackTrace: stackTrace); + } + } +} diff --git a/lib/app/services/riverpod/riverpod_logger.dart b/lib/app/services/riverpod/riverpod_logger.dart deleted file mode 100644 index f3f31ca73..000000000 --- a/lib/app/services/riverpod/riverpod_logger.dart +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-License-Identifier: ice License 1.0 - -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:ion/app/services/logger/logger.dart'; - -class RiverpodLogger extends ProviderObserver { - static const String tag = 'Riverpod'; - - @override - void didAddProvider( - ProviderBase provider, - Object? value, - ProviderContainer container, - ) { - _log('Provider $provider was initialized with $value'); - } - - @override - void didDisposeProvider( - ProviderBase provider, - ProviderContainer container, - ) { - _log('Provider $provider was disposed'); - } - - @override - void didUpdateProvider( - ProviderBase provider, - Object? previousValue, - Object? newValue, - ProviderContainer container, - ) { - _log('Provider $provider updated from $previousValue to $newValue'); - } - - @override - void providerDidFail( - ProviderBase provider, - Object error, - StackTrace stackTrace, - ProviderContainer container, - ) { - _log( - 'Provider $provider threw error', - error: error, - stackTrace: stackTrace, - ); - } - - void _log( - String message, { - Object? error, - StackTrace? stackTrace, - }) { - Logger.log(message, name: tag, error: error, stackTrace: stackTrace); - } -} diff --git a/lib/app/services/riverpod/root_provider_scope.dart b/lib/app/services/riverpod/root_provider_scope.dart deleted file mode 100644 index a4467e275..000000000 --- a/lib/app/services/riverpod/root_provider_scope.dart +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: ice License 1.0 - -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:ion/app/services/logger/config.dart'; -import 'package:ion/app/services/riverpod/riverpod_logger.dart'; - -class RiverpodRootProviderScope extends StatelessWidget { - const RiverpodRootProviderScope({ - required this.child, - super.key, - }); - - final Widget child; - - @override - Widget build(BuildContext context) { - return ProviderScope( - observers: [ - if (LoggerConfig.riverpodLogsEnabled) RiverpodLogger(), - ], - child: child, - ); - } -} diff --git a/lib/main.dart b/lib/main.dart index aa2e59bf7..645c01b89 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ // SPDX-License-Identifier: ice License 1.0 import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_quill/translations.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:ion/app/features/core/providers/app_locale_provider.c.dart'; @@ -12,18 +13,24 @@ import 'package:ion/app/features/wallet/components/coin_sync/coins_sync_wrapper. import 'package:ion/app/router/components/app_router_builder.dart'; import 'package:ion/app/router/components/modal_wrapper/sheet_scope.dart'; import 'package:ion/app/router/providers/go_router_provider.c.dart'; -import 'package:ion/app/services/logger/config.dart'; -import 'package:ion/app/services/riverpod/riverpod_logger.dart'; import 'package:ion/app/services/storage/secure_storage.c.dart'; import 'package:ion/app/theme/theme.dart'; import 'package:ion/generated/app_localizations.dart'; +import 'package:ion/generated/assets.gen.dart'; +import 'package:talker_riverpod_logger/talker_riverpod_logger_observer.dart'; + +const _riverpodLoggerEnabled = false; void main() async { WidgetsFlutterBinding.ensureInitialized(); await SecureStorage().clearOnReinstall(); + await dotenv.load(fileName: Assets.aApp); + runApp( ProviderScope( - observers: [if (LoggerConfig.riverpodLogsEnabled) RiverpodLogger()], + observers: [ + if (_riverpodLoggerEnabled) TalkerRiverpodObserver(), + ], child: const IONApp(), ), ); diff --git a/packages/ion_identity_client/example/pubspec.lock b/packages/ion_identity_client/example/pubspec.lock index 2ebcaaa19..754b02635 100644 --- a/packages/ion_identity_client/example/pubspec.lock +++ b/packages/ion_identity_client/example/pubspec.lock @@ -772,14 +772,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" - pretty_dio_logger: - dependency: transitive - description: - name: pretty_dio_logger - sha256: "36f2101299786d567869493e2f5731de61ce130faa14679473b26905a92b6407" - url: "https://pub.dev" - source: hosted - version: "1.4.0" pub_semver: dependency: transitive description: diff --git a/packages/ion_identity_client/lib/src/core/service_locator/network_service_locator.dart b/packages/ion_identity_client/lib/src/core/service_locator/network_service_locator.dart index e63c46d90..90f3eea1c 100644 --- a/packages/ion_identity_client/lib/src/core/service_locator/network_service_locator.dart +++ b/packages/ion_identity_client/lib/src/core/service_locator/network_service_locator.dart @@ -9,7 +9,6 @@ import 'package:ion_identity_client/src/core/service_locator/ion_identity_client import 'package:ion_identity_client/src/core/storage/private_key_storage.dart'; import 'package:ion_identity_client/src/core/storage/token_storage.dart'; import 'package:ion_identity_client/src/core/types/request_headers.dart'; -import 'package:pretty_dio_logger/pretty_dio_logger.dart'; class NetworkServiceLocator with _Dio, _Interceptors, _TokenStorage, _PrivateKeyStorage, _NetworkClient { @@ -64,7 +63,7 @@ mixin _Dio { final dio = Dio(dioOptions); final interceptors = [ - if (config.logging) NetworkServiceLocator().loggerInterceptor(), + if (config.logger != null) config.logger!, ]; dio.interceptors.addAll(interceptors); @@ -76,8 +75,9 @@ mixin _Interceptors { Iterable interceptors({ required IONIdentityConfig config, }) { + final logger = config.logger; return [ - if (config.logging) loggerInterceptor(), + if (config.logger != null) logger!, authInterceptor(config: config), ]; } @@ -90,13 +90,6 @@ mixin _Interceptors { delegatedLoginService: AuthClientServiceLocator().delegatedLogin(config: config), ); } - - Interceptor loggerInterceptor() { - return PrettyDioLogger( - requestBody: true, - requestHeader: true, - ); - } } mixin _TokenStorage { diff --git a/packages/ion_identity_client/lib/src/ion_identity_config.dart b/packages/ion_identity_client/lib/src/ion_identity_config.dart index 8c39e1240..b16781b29 100644 --- a/packages/ion_identity_client/lib/src/ion_identity_config.dart +++ b/packages/ion_identity_client/lib/src/ion_identity_config.dart @@ -1,5 +1,7 @@ // SPDX-License-Identifier: ice License 1.0 +import 'package:dio/dio.dart'; + /// A configuration class for the ION Identity client, containing the necessary /// identifiers and origin information required to initialize the client. class IONIdentityConfig { @@ -8,7 +10,7 @@ class IONIdentityConfig { IONIdentityConfig({ required this.appId, required this.origin, - this.logging = false, + this.logger, }); /// The application identifier used to uniquely identify the app within the ION Identity API. @@ -18,9 +20,10 @@ class IONIdentityConfig { /// and securing API requests. final String origin; - /// Whether to enable logging for the ION Identity client. - final bool logging; + /// The logger interceptor to use for logging requests and responses. + final Interceptor? logger; @override - String toString() => 'IONIdentityConfig(appId: $appId, origin: $origin, logging: $logging)'; + String toString() => + 'IONIdentityConfig(appId: $appId, origin: $origin, logger: ${logger?.toString() ?? 'null'})'; } diff --git a/packages/ion_identity_client/pubspec.yaml b/packages/ion_identity_client/pubspec.yaml index c29ece699..88283aad7 100644 --- a/packages/ion_identity_client/pubspec.yaml +++ b/packages/ion_identity_client/pubspec.yaml @@ -22,7 +22,6 @@ dependencies: local_auth: ^2.3.0 passkeys: ^2.1.0 pointycastle: ^3.9.1 - pretty_dio_logger: ^1.4.0 rxdart: ^0.28.0 sprintf: ^7.0.0 uuid: ^4.5.1 diff --git a/pubspec.lock b/pubspec.lock index 2ebccb08b..9e9795e70 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -46,6 +46,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.2+1" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" + url: "https://pub.dev" + source: hosted + version: "2.0.3" archive: dependency: transitive description: @@ -98,10 +106,10 @@ packages: dependency: transitive description: name: bip340 - sha256: "2a92f6ed68959f75d67c9a304c17928b9c9449587d4f75ee68f34152f7f69e87" + sha256: b7bcd70a860e605046006adaa72bc4f7453f4d31d7ba74a4ad9d5de387a0fc0b url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.3.0" boolean_selector: dependency: transitive description: @@ -346,10 +354,10 @@ packages: dependency: transitive description: name: crypto - sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.6" crypto_keys: dependency: transitive description: @@ -546,10 +554,10 @@ packages: dependency: transitive description: name: equatable - sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.7" es_compression: dependency: "direct main" description: @@ -1018,14 +1026,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" - get_it: - dependency: transitive - description: - name: get_it - sha256: d85128a5dae4ea777324730dc65edd9c9f43155c109d5cc0a69cab74139fbac1 - url: "https://pub.dev" - source: hosted - version: "7.7.0" glob: dependency: transitive description: @@ -1058,6 +1058,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + group_button: + dependency: transitive + description: + name: group_button + sha256: "0610fcf28ed122bfb4b410fce161a390f7f2531d55d1d65c5375982001415940" + url: "https://pub.dev" + source: hosted + version: "5.3.4" hashcodes: dependency: transitive description: @@ -1344,14 +1352,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.11" - logger: - dependency: transitive - description: - name: logger - sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 - url: "https://pub.dev" - source: hosted - version: "2.5.0" logging: dependency: transitive description: @@ -1469,11 +1469,11 @@ packages: dependency: "direct main" description: path: "." - ref: "0.0.17" - resolved-ref: "4dcd288c2c842725fa2cb096cb43142f8f1a390d" + ref: "0.0.18" + resolved-ref: ae7a435d3eb28edc177a34004edbb523ba9ed264 url: "https://github.com/ice-blockchain/nostr-dart.git" source: git - version: "0.0.17" + version: "0.0.18" octo_image: dependency: transitive description: @@ -1731,14 +1731,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" - pretty_dio_logger: - dependency: "direct main" - description: - name: pretty_dio_logger - sha256: "36f2101299786d567869493e2f5731de61ce130faa14679473b26905a92b6407" - url: "https://pub.dev" - source: hosted - version: "1.4.0" process: dependency: transitive description: @@ -1908,6 +1900,38 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.9" + shake_gesture: + dependency: "direct main" + description: + name: shake_gesture + sha256: "943a24bbb34c9c23036d78d60934553435649aa6a8c86ac1b1d58d00ca5f5d06" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + shake_gesture_android: + dependency: transitive + description: + name: shake_gesture_android + sha256: cca62005b4a3beff16fe023e25ec70eee9cfbb1bef215dc1ff5b8a030c3b7fce + url: "https://pub.dev" + source: hosted + version: "1.1.4" + shake_gesture_ios: + dependency: transitive + description: + name: shake_gesture_ios + sha256: dd7d5a89587f3696d2abe45d35c350e9986ebaa723538892e6da91d4c7e092d5 + url: "https://pub.dev" + source: hosted + version: "1.0.2" + shake_gesture_platform_interface: + dependency: transitive + description: + name: shake_gesture_platform_interface + sha256: c5e9adef94892c16e030aabd2c480620cac4dfc002f397309e796248a2a397cc + url: "https://pub.dev" + source: hosted + version: "1.0.1" share_plus: dependency: "direct main" description: @@ -2145,6 +2169,46 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0+1" + talker: + dependency: transitive + description: + name: talker + sha256: dff22de27ac0dbe1bb2052fc258ccceee0a2703e6cfe03839ccd337aaf5d291b + url: "https://pub.dev" + source: hosted + version: "4.6.0" + talker_dio_logger: + dependency: "direct main" + description: + name: talker_dio_logger + sha256: "365f42501e32af3cb91050af01d8de0c1c0a6ec72b74c48d18d5ea25982de210" + url: "https://pub.dev" + source: hosted + version: "4.5.4" + talker_flutter: + dependency: "direct main" + description: + name: talker_flutter + sha256: "7dce55a42ffe9ac590d46f71a978aa1eb7aecd040915a8307f8880526c47f9ae" + url: "https://pub.dev" + source: hosted + version: "4.5.4" + talker_logger: + dependency: transitive + description: + name: talker_logger + sha256: f6af15f038576fb727668131c2a829f986bab0bad379e48805a8f251e3687002 + url: "https://pub.dev" + source: hosted + version: "4.6.0" + talker_riverpod_logger: + dependency: "direct main" + description: + name: talker_riverpod_logger + sha256: "3675743a5f78fb36bb060fda1a313efe3ee3bf16e999c7163cd5893b11a2aaf8" + url: "https://pub.dev" + source: hosted + version: "4.5.4" term_glyph: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3f6490946..3891f4860 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -70,7 +70,7 @@ dependencies: nostr_dart: git: url: https://github.com/ice-blockchain/nostr-dart.git - ref: 0.0.17 + ref: 0.0.18 ogp_data_extract: ^0.1.4 package_info_plus: ^4.2.0 path: ^1.9.0 @@ -78,7 +78,6 @@ dependencies: permission_handler: ^11.3.0 photo_manager: ^3.6.3 photo_manager_image_provider: ^2.1.1 - pretty_dio_logger: ^1.4.0 qr_code_scanner: git: url: https://github.com/ice-blockchain/qr_code_scanner @@ -86,10 +85,14 @@ dependencies: version: ^1.0.0 qr_flutter: ^4.1.0 riverpod_annotation: ^2.6.1 + shake_gesture: ^1.1.0 share_plus: ^10.0.2 shared_preferences: ^2.2.2 shimmer: ^3.0.0 smooth_sheets: ^1.0.0-f324.0.10.2 + talker_dio_logger: 4.5.4 + talker_flutter: 4.5.4 + talker_riverpod_logger: 4.5.4 timeago: ^3.6.1 timeago_flutter: ^3.6.0 uri_to_file: ^1.0.0 diff --git a/test/user/follow_list_test.dart b/test/user/follow_list_test.dart index 35dd98e73..47d590086 100644 --- a/test/user/follow_list_test.dart +++ b/test/user/follow_list_test.dart @@ -6,7 +6,7 @@ import 'package:ion/app/services/nostr/ed25519_key_store.dart'; import 'package:nostr_dart/nostr_dart.dart'; void main() { - NostrDart.configure(logLevel: NostrLogLevel.ALL); + NostrDart.configure(); group('FollowList Tests', () { test('FollowList.fromEventMessage should work when all data is there', () async { diff --git a/test/user/interests_set_test.dart b/test/user/interests_set_test.dart index 8ba69e9dc..14918fb59 100644 --- a/test/user/interests_set_test.dart +++ b/test/user/interests_set_test.dart @@ -6,7 +6,7 @@ import 'package:ion/app/services/nostr/ed25519_key_store.dart'; import 'package:nostr_dart/nostr_dart.dart'; void main() { - NostrDart.configure(logLevel: NostrLogLevel.ALL); + NostrDart.configure(); group('InterestsSet Tests', () { test('InterestSet.fromEventMessage should work when all data is there', () async { diff --git a/test/user/interests_test.dart b/test/user/interests_test.dart index ca72e6223..8ecb8ea28 100644 --- a/test/user/interests_test.dart +++ b/test/user/interests_test.dart @@ -7,7 +7,7 @@ import 'package:ion/app/services/nostr/ed25519_key_store.dart'; import 'package:nostr_dart/nostr_dart.dart'; void main() { - NostrDart.configure(logLevel: NostrLogLevel.ALL); + NostrDart.configure(); group('Interests Tests', () { test('Interests.fromEventMessage should work when all data is there', () async { diff --git a/test/user/user_delegation_test.dart b/test/user/user_delegation_test.dart index 61dead59e..f104715c2 100644 --- a/test/user/user_delegation_test.dart +++ b/test/user/user_delegation_test.dart @@ -7,7 +7,7 @@ import 'package:nostr_dart/nostr_dart.dart'; void main() { const pubkey = '477318cfb5427b9cfc66a9fa376150c1ddbc62115ae27cef72417eb959691396'; - NostrDart.configure(logLevel: NostrLogLevel.ALL); + NostrDart.configure(); group('UserDelegation Tests', () { test('UserDelegation.fromEventMessage should work when all data is there', () async {