diff --git a/android/app/build.gradle b/android/app/build.gradle index 1ebd5a9b..b4f1f08b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -47,7 +47,7 @@ android { applicationId "com.webreinvent.vaahflutter" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. - minSdkVersion flutter.minSdkVersion + minSdkVersion 19 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ab80b9a6..84c4431e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -24,6 +24,12 @@ + + + + + + diff --git a/ios/Podfile b/ios/Podfile index 88359b22..e98a4545 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -37,5 +37,8 @@ end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) + target.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0' + end end end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 9e4b38ac..9771e359 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -33,14 +33,40 @@ PODS: - file_picker (0.0.1): - DKImagePickerController/PhotoGallery - Flutter + - Firebase/CoreOnly (10.3.0): + - FirebaseCore (= 10.3.0) + - Firebase/DynamicLinks (10.3.0): + - Firebase/CoreOnly + - FirebaseDynamicLinks (~> 10.3.0) + - firebase_core (2.7.1): + - Firebase/CoreOnly (= 10.3.0) + - Flutter + - firebase_dynamic_links (5.0.16): + - Firebase/DynamicLinks (= 10.3.0) + - firebase_core + - Flutter + - FirebaseCore (10.3.0): + - FirebaseCoreInternal (~> 10.0) + - GoogleUtilities/Environment (~> 7.8) + - GoogleUtilities/Logger (~> 7.8) + - FirebaseCoreInternal (10.5.0): + - "GoogleUtilities/NSData+zlib (~> 7.8)" + - FirebaseDynamicLinks (10.3.0): + - FirebaseCore (~> 10.0) - Flutter (1.0.0) - fluttertoast (0.0.2): - Flutter - Toast + - GoogleUtilities/Environment (7.11.0): + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/Logger (7.11.0): + - GoogleUtilities/Environment + - "GoogleUtilities/NSData+zlib (7.11.0)" - package_info_plus (0.4.5): - Flutter - path_provider_ios (0.0.1): - Flutter + - PromisesObjC (2.2.0) - SDWebImage (5.14.2): - SDWebImage/Core (= 5.14.2) - SDWebImage/Core (5.14.2) @@ -54,6 +80,8 @@ PODS: DEPENDENCIES: - file_picker (from `.symlinks/plugins/file_picker/ios`) + - firebase_core (from `.symlinks/plugins/firebase_core/ios`) + - firebase_dynamic_links (from `.symlinks/plugins/firebase_dynamic_links/ios`) - Flutter (from `Flutter`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) @@ -64,6 +92,12 @@ SPEC REPOS: trunk: - DKImagePickerController - DKPhotoGallery + - Firebase + - FirebaseCore + - FirebaseCoreInternal + - FirebaseDynamicLinks + - GoogleUtilities + - PromisesObjC - SDWebImage - Sentry - SwiftyGif @@ -72,6 +106,10 @@ SPEC REPOS: EXTERNAL SOURCES: file_picker: :path: ".symlinks/plugins/file_picker/ios" + firebase_core: + :path: ".symlinks/plugins/firebase_core/ios" + firebase_dynamic_links: + :path: ".symlinks/plugins/firebase_dynamic_links/ios" Flutter: :path: Flutter fluttertoast: @@ -87,9 +125,18 @@ SPEC CHECKSUMS: DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 file_picker: ce3938a0df3cc1ef404671531facef740d03f920 + Firebase: f92fc551ead69c94168d36c2b26188263860acd9 + firebase_core: 1ae9f9aa76e6e1edc14fb181637ad466fd6c6fa4 + firebase_dynamic_links: 42ab4dd387b07d5c7af2e9aa63a7b5fa3518dd8b + FirebaseCore: 988754646ab3bd4bdcb740f1bfe26b9f6c0d5f2a + FirebaseCoreInternal: e463f41bb935cd049505bf7e9a5bdd7dcea90df6 + FirebaseDynamicLinks: 51c81d07bd63155bb56d76b0abdda79c8a3d8d02 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0 + GoogleUtilities: c2bdc4cf2ce786c4d2e6b3bcfd599a25ca78f06f + package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 + PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 Sentry: 4c9babff9034785067c896fd580b1f7de44da020 sentry_flutter: b10ae7a5ddcbc7f04648eeb2672b5747230172f1 diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements new file mode 100644 index 00000000..56178028 --- /dev/null +++ b/ios/Runner/Runner.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.associated-domains + + applinks:YOUR_FIREBASE_APP_DYNAMIC_LINK_PREFIX.page.link + + + diff --git a/lib/main.dart b/lib/main.dart index fc29ccae..8cbd3461 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,5 +7,7 @@ import './vaahextendflutter/base/base_controller.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); BaseController baseController = Get.put(BaseController()); - await baseController.init(const AppConfig()); // Pass main app as argument in init method + await baseController.init( + app: const AppConfig(), + ); // Pass main app as argument in init method } diff --git a/lib/vaahextendflutter/base/base_controller.dart b/lib/vaahextendflutter/base/base_controller.dart index 0947a271..db4290dd 100644 --- a/lib/vaahextendflutter/base/base_controller.dart +++ b/lib/vaahextendflutter/base/base_controller.dart @@ -1,25 +1,41 @@ import 'dart:async'; +import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -import '../../controllers/root_assets_controller.dart'; import '../app_theme.dart'; import '../env.dart'; import '../services/api.dart'; +import '../services/dynamic_links.dart'; class BaseController extends GetxController { - Future init(Widget app) async { + Future init({ + required Widget app, + FirebaseOptions? firebaseOptions, + }) async { + // Storage initialization to store some properties locally await GetStorage.init(); + + // Environment initialization EnvironmentConfig.setEnvConfig(); + final EnvironmentConfig config = EnvironmentConfig.getEnvConfig(); + + // Initialization of Firebase and Services + if (firebaseOptions != null) { + await Firebase.initializeApp( + options: firebaseOptions, + ); + DynamicLinks.init(); + } + + // Other Local Initializations (Depends on your app) AppTheme.init(); Api.init(); - Get.put(RootAssetsController()); - - final EnvironmentConfig config = EnvironmentConfig.getEnvConfig(); + // Sentry Initialization (And/ Or) Running main app if (null != config.sentryConfig && config.sentryConfig!.dsn.isNotEmpty) { await SentryFlutter.init( (options) => options @@ -44,8 +60,10 @@ class BaseController extends GetxController { child: child, ); } + // Running main app runApp(child); } else { + // Running main app when sentry config is not there runApp(app); } } diff --git a/lib/vaahextendflutter/env.dart b/lib/vaahextendflutter/env.dart index 2229d62e..150d8e57 100644 --- a/lib/vaahextendflutter/env.dart +++ b/lib/vaahextendflutter/env.dart @@ -26,7 +26,7 @@ final EnvironmentConfig defaultConfig = EnvironmentConfig( enableCloudLogs: true, enableApiLogInterceptor: true, showDebugPanel: true, - debugPanelColor: AppTheme.colors['black']!.withOpacity(0.7), + debugPanelColor: AppTheme.colors['black']!.withOpacity(0.8), ); // To add new configuration add new key, value pair in envConfigs diff --git a/lib/vaahextendflutter/services/dynamic_links.dart b/lib/vaahextendflutter/services/dynamic_links.dart new file mode 100644 index 00000000..27c44b9f --- /dev/null +++ b/lib/vaahextendflutter/services/dynamic_links.dart @@ -0,0 +1,108 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:firebase_dynamic_links/firebase_dynamic_links.dart'; +import 'package:get/get.dart'; + +import './logging_library/logging_library.dart'; + +abstract class DynamicLinks { + static void init() async { + // handle initial dynamic link + final getInitialLink = await _firebaseDynamicLinks.getInitialLink(); + if (getInitialLink != null) { + _handleLink(getInitialLink); + } + + // listen/ subscribe to the links which comes later and handle them + _firebaseDynamicLinks.onLink + .listen( + _handleLink, + ) + .onError( + (error, stackTrace) => Log.exception(error, stackTrace: stackTrace), + ); + } + + static final FirebaseDynamicLinks _firebaseDynamicLinks = FirebaseDynamicLinks.instance; + static final StreamController _dynamicLinksStreamController = + StreamController.broadcast(); + static final Stream dynamicLinksStream = _dynamicLinksStreamController.stream; + + static Future createLink({ + required String? path, + required dynamic data, + required dynamic auth, + }) async { + try { + final String parameters = jsonEncode({"path": path, "data": data, "auth": auth}); + return await _firebaseDynamicLinks.buildShortLink( + DynamicLinkParameters( + link: Uri.parse("https://your.domain?payload=$parameters"), + uriPrefix: "https://YOUR_FIREBASE_APP_DYNAMIC_LINK_PREFIX.page.link", + androidParameters: const AndroidParameters(packageName: "your.package.name"), + iosParameters: const IOSParameters(bundleId: "your.bundle.identifier"), + ), + shortLinkType: ShortDynamicLinkType.unguessable, + ); + } catch (error, stackTrace) { + Log.exception(error, stackTrace: stackTrace, hint: "Error creating dynamic link!"); + return null; + } + } + + static Future _handleLink(PendingDynamicLinkData linkData) async { + try { + final Uri decodedLink = Uri.parse(Uri.decodeFull(linkData.link.toString())); + final dynamic payload = _decodePayload(decodedLink); + _dynamicLinksStreamController.add( + DeepLink( + encoded: linkData.link.toString(), + decoded: "${linkData.link.host}${linkData.link.path}?payload=$payload", + ), + ); + Log.success({ + "encoded": linkData.link.toString(), + "decoded": "${linkData.link.host}${linkData.link.path}?payload=$payload", + }); + if (payload != null && payload['path'] != null) { + Get.offAllNamed( + payload['path'], + arguments: { + 'data': payload['data'], + 'auth': payload['auth'], + }, + ); + } + } catch (error, stackTrace) { + Log.exception( + error, + stackTrace: stackTrace, + hint: "Error handling dynamic link! ${linkData.asMap()}", + ); + } + } + + static dynamic _decodePayload(Uri link) { + try { + return jsonDecode(link.queryParameters['payload'].toString()); + } catch (error, stackTrace) { + Log.exception( + error, + stackTrace: stackTrace, + hint: "Error decoding payload! $link", + ); + return null; + } + } +} + +class DeepLink { + final String encoded; + final String decoded; + + const DeepLink({ + required this.encoded, + required this.decoded, + }); +} diff --git a/lib/vaahextendflutter/services/logging_library/logging_library.dart b/lib/vaahextendflutter/services/logging_library/logging_library.dart index fb20f2e3..e4461423 100644 --- a/lib/vaahextendflutter/services/logging_library/logging_library.dart +++ b/lib/vaahextendflutter/services/logging_library/logging_library.dart @@ -77,7 +77,7 @@ class Log { bool disableCloudLogging = false, }) { if (_config.enableLocalLogs && !disableLocalLogging) { - Console.danger(throwable.toString(), data); + Console.danger('$throwable\n$hint', data); } if (_config.enableCloudLogs && !disableCloudLogging) { final hintWithData = { diff --git a/lib/vaahextendflutter/widgets/atoms/app_expansion_panel.dart b/lib/vaahextendflutter/widgets/atoms/app_expansion_panel.dart index 6a7801fc..6b0f6e22 100644 --- a/lib/vaahextendflutter/widgets/atoms/app_expansion_panel.dart +++ b/lib/vaahextendflutter/widgets/atoms/app_expansion_panel.dart @@ -143,7 +143,8 @@ class _AppExpansionPanelState extends State @immutable class AppExpansionPanelIcon extends StatelessWidget { - const AppExpansionPanelIcon({Key? key}) : super(key: key); + final Color? color; + const AppExpansionPanelIcon({Key? key, this.color}) : super(key: key); @override Widget build(BuildContext context) { @@ -151,7 +152,7 @@ class AppExpansionPanelIcon extends StatelessWidget { turns: context.findAncestorStateOfType<_AppExpansionPanelState>()!._iconTurns, child: FaIcon( FontAwesomeIcons.angleDown, - color: AppTheme.colors['primary'], + color: color ?? AppTheme.colors['primary'], ), ); } diff --git a/lib/vaahextendflutter/widgets/debug.dart b/lib/vaahextendflutter/widgets/debug.dart index 5831dd21..ae81feae 100644 --- a/lib/vaahextendflutter/widgets/debug.dart +++ b/lib/vaahextendflutter/widgets/debug.dart @@ -8,10 +8,13 @@ // ***************************************** import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import '../app_theme.dart'; import '../env.dart'; import '../helpers/constants.dart'; import '../helpers/styles.dart'; +import '../services/dynamic_links.dart'; const double constHandleWidth = 180.0; // tag handle width const double constHandleHeight = 38.0; // tag handle height @@ -85,111 +88,200 @@ class DebugWidgetState extends State with SingleTickerProviderState @override Widget build(BuildContext context) { + final double topMargin = MediaQuery.of(context).padding.top + defaultMargin; return showDebugPanel ? LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { - final height = constraints.maxHeight; + final height = constraints.maxHeight - topMargin; final minFactor = (_handleHeight / height); return Stack( fit: StackFit.expand, children: [ widget.child, - GestureDetector( - onVerticalDragDown: (DragDownDetails details) { - _controller.stop(); - }, - onVerticalDragUpdate: (DragUpdateDetails details) { - _controller.value += (-details.primaryDelta! / height); - }, - onVerticalDragEnd: (DragEndDetails details) { - if (_controller.isDismissed) { - return; - } - if (details.primaryVelocity!.abs() >= 365.0) { - final visualVelocity = -details.primaryVelocity! / height; - _controller.fling(velocity: visualVelocity); - } else if (_controller.value < 0.5) { - close(); - } else { - open(); - } - }, - onVerticalDragCancel: () { - if (_controller.isDismissed || _controller.isAnimating) { - return; - } - if (_controller.value < 0.5) { - close(); - } else { - open(); - } - }, - excludeFromSemantics: true, - child: RepaintBoundary( - child: Align( - alignment: Alignment.bottomCenter, - child: AnimatedBuilder( - animation: _controller, - builder: (BuildContext context, Widget? child) { - return Align( - alignment: Alignment.topCenter, - heightFactor: _controller.value + minFactor, - child: child, - ); - }, - child: RepaintBoundary( - child: FocusScope( - key: _drawerKey, - node: _focusScopeNode, - child: _EnvPanel( - handleHeight: _handleHeight, - onHandlePressed: toggle, - config: _environmentConfig, - child: Builder( - builder: (BuildContext context) { - return ListView( - primary: false, - padding: EdgeInsets.only( - top: MediaQuery.of(context).padding.top + - defaultPadding + - _handleHeight, - bottom: defaultPadding, - left: defaultPadding, - right: defaultPadding, - ), - children: [ - ..._showDetails( - [ - 'App Title: ${_environmentConfig.appTitle}', - 'App Title Short: ${_environmentConfig.appTitleShort}', - _environmentConfig.envType, - 'Version: ${_environmentConfig.version}', - 'Build: ${_environmentConfig.build}', - ], + Container( + margin: EdgeInsets.only(top: topMargin), + child: GestureDetector( + onVerticalDragDown: (DragDownDetails details) { + _controller.stop(); + }, + onVerticalDragUpdate: (DragUpdateDetails details) { + _controller.value += (-details.primaryDelta! / height); + }, + onVerticalDragEnd: (DragEndDetails details) { + if (_controller.isDismissed) { + return; + } + if (details.primaryVelocity!.abs() >= 365.0) { + final visualVelocity = -details.primaryVelocity! / height; + _controller.fling(velocity: visualVelocity); + } else if (_controller.value < 0.5) { + close(); + } else { + open(); + } + }, + onVerticalDragCancel: () { + if (_controller.isDismissed || _controller.isAnimating) { + return; + } + if (_controller.value < 0.5) { + close(); + } else { + open(); + } + }, + excludeFromSemantics: true, + child: RepaintBoundary( + child: Align( + alignment: Alignment.bottomCenter, + child: AnimatedBuilder( + animation: _controller, + builder: (BuildContext context, Widget? child) { + return Align( + alignment: Alignment.topCenter, + heightFactor: _controller.value + minFactor, + child: child, + ); + }, + child: RepaintBoundary( + child: FocusScope( + key: _drawerKey, + node: _focusScopeNode, + child: _EnvPanel( + handleHeight: _handleHeight, + onHandlePressed: toggle, + config: _environmentConfig, + child: Builder( + builder: (BuildContext context) { + return Padding( + padding: EdgeInsets.only( + top: _handleHeight, ), - verticalMargin24, - ..._showDetails( - [ - 'Backend URL: ${_environmentConfig.backendUrl}', - 'API URL: ${_environmentConfig.apiUrl}', - 'Request and Response Timeout: ${(_environmentConfig.timeoutLimit) / 1000} Seconds', - 'Firebase Id: ${_environmentConfig.firebaseId}', - 'Local Logs Enabled (Console + Device Specific): ${_environmentConfig.enableLocalLogs}', - 'Cloud Logs Enabled: ${_environmentConfig.enableCloudLogs}', - if (null != _environmentConfig.sentryConfig) ...[ - 'Sentry DSN: ${_environmentConfig.sentryConfig!.dsn}', - 'Sentry Auto App Start (Record Cold And Warm Start Time): ${_environmentConfig.sentryConfig!.autoAppStart}', - 'Sentry Traces Sample Rate: ${_environmentConfig.sentryConfig!.tracesSampleRate}', - 'Sentry User Interaction Tracing: ${_environmentConfig.sentryConfig!.enableUserInteractionTracing}', - 'Sentry Auto Performance Tracking: ${_environmentConfig.sentryConfig!.enableAutoPerformanceTracking}', - 'Sentry Assets Instrumentation: ${_environmentConfig.sentryConfig!.enableAssetsInstrumentation}', - ], - 'API Logs Interceptor: ${_environmentConfig.enableApiLogInterceptor}', - ], + child: Container( + margin: allPadding24, + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + children: [ + _ShowDetails( + contentHolder: PanelDataContentHolder( + content: { + 'App Title': + Data(value: _environmentConfig.appTitle), + 'App Title Short': Data( + value: _environmentConfig.appTitleShort, + ), + 'Environment': Data( + value: _environmentConfig.envType, + ), + 'Version': + Data(value: _environmentConfig.version), + 'Build': + Data(value: _environmentConfig.build), + 'Backend URL': Data( + value: _environmentConfig.backendUrl, + ), + 'API URL': + Data(value: _environmentConfig.apiUrl), + 'Request and Response Timeout': Data( + value: + '${_environmentConfig.timeoutLimit / 1000} Seconds', + ), + 'Firebase Id': Data( + value: _environmentConfig.firebaseId, + ), + 'API Logs Interceptor': Data( + value: _environmentConfig + .enableApiLogInterceptor + ? 'enabled' + : 'disabled', + color: _environmentConfig + .enableApiLogInterceptor + ? AppTheme.colors['success'] + : AppTheme.colors['danger'], + ), + 'Local Logs': Data( + value: _environmentConfig.enableLocalLogs + ? 'enabled' + : 'disabled', + color: _environmentConfig.enableLocalLogs + ? AppTheme.colors['success'] + : AppTheme.colors['danger'], + ), + 'Cloud Logs': Data( + value: _environmentConfig.enableCloudLogs + ? 'enabled' + : 'disabled', + color: _environmentConfig.enableCloudLogs + ? AppTheme.colors['success'] + : AppTheme.colors['danger'], + ), + if (null != + _environmentConfig.sentryConfig) ...{ + 'Sentry DSN': Data( + value: + _environmentConfig.sentryConfig!.dsn, + ), + 'Sentry Traces Sample Rate': Data( + value: _environmentConfig + .sentryConfig!.tracesSampleRate + .toString(), + ), + 'Sentry Auto App Start (Record Cold And Warm Start Time)': + Data( + value: _environmentConfig + .sentryConfig!.autoAppStart + ? 'enabled' + : 'disabled', + color: _environmentConfig + .sentryConfig!.autoAppStart + ? AppTheme.colors['success'] + : AppTheme.colors['danger'], + ), + 'Sentry User Interaction Tracing': Data( + value: _environmentConfig.sentryConfig! + .enableUserInteractionTracing + ? 'enabled' + : 'disabled', + color: _environmentConfig.sentryConfig! + .enableUserInteractionTracing + ? AppTheme.colors['success'] + : AppTheme.colors['danger'], + ), + 'Sentry Auto Performance Tracking': Data( + value: _environmentConfig.sentryConfig! + .enableAutoPerformanceTracking + ? 'enabled' + : 'disabled', + color: _environmentConfig.sentryConfig! + .enableAutoPerformanceTracking + ? AppTheme.colors['success'] + : AppTheme.colors['danger'], + ), + 'Sentry Assets Instrumentation': Data( + value: _environmentConfig.sentryConfig! + .enableAssetsInstrumentation + ? 'enabled' + : 'disabled', + color: _environmentConfig.sentryConfig! + .enableAssetsInstrumentation + ? AppTheme.colors['success'] + : AppTheme.colors['danger'], + ), + }, + }, + ), + ), + verticalMargin24, + const _StreamLinksSection(), + verticalMargin24, + ], + ), + ), ), - ], - ); - }, + ); + }, + ), ), ), ), @@ -204,24 +296,6 @@ class DebugWidgetState extends State with SingleTickerProviderState ) : widget.child; } - - List _showDetails(List details) { - return details - .map( - (detail) => Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SelectableText( - detail, - style: TextStyles.regular3, - ), - verticalMargin8, - ], - ), - ) - .toList(growable: false); - } } @immutable @@ -379,3 +453,143 @@ class _PanelBorder extends ShapeBorder { // } } + +class _StreamLinksSection extends StatefulWidget { + const _StreamLinksSection({Key? key}) : super(key: key); + + @override + State<_StreamLinksSection> createState() => _StreamLinksSectionState(); +} + +class _StreamLinksSectionState extends State<_StreamLinksSection> { + DeepLink? link; + + @override + void initState() { + super.initState(); + DynamicLinks.dynamicLinksStream.listen((DeepLink deeplink) { + setState(() { + link = deeplink; + }); + }); + } + + @override + Widget build(BuildContext context) { + return link == null + ? emptyWidget + : Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Dynamic Link'), + verticalMargin8, + _ShowDetails(contentHolder: PanelLinkContentHolder(content: link!)), + ], + ); + } +} + +class _ShowDetails extends StatefulWidget { + final PanelContentHolder contentHolder; + + const _ShowDetails({ + Key? key, + required this.contentHolder, + }) : super(key: key); + + @override + State<_ShowDetails> createState() => _ShowDetailsState(); +} + +class _ShowDetailsState extends State<_ShowDetails> { + @override + Widget build(BuildContext context) { + final PanelContentHolder contentHolder = widget.contentHolder; + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (contentHolder is PanelDataContentHolder) + Builder( + builder: (context) { + final List rows = []; + contentHolder.content.forEach( + (key, data) { + rows.add( + TableRow( + children: [ + Padding( + padding: allPadding8, + child: Text(key), + ), + Padding( + padding: allPadding8, + child: SelectableText( + data.value ?? '', + style: TextStyle(color: data.color ?? AppTheme.colors['warning']), + ), + ), + ], + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: AppTheme.colors['white']!.withOpacity(0.55)), + ), + ), + ), + ); + }, + ); + return Table( + children: rows, + ); + }, + ) + else if (contentHolder is PanelLinkContentHolder) ...[ + SelectableText( + contentHolder.content.encoded, + style: TextStyles.regular3?.copyWith(color: AppTheme.colors['danger']), + onTap: () => Clipboard.setData(ClipboardData(text: contentHolder.content.encoded)), + ), + SelectableText( + contentHolder.content.decoded, + style: TextStyles.regular3?.copyWith(color: AppTheme.colors['success']), + onTap: () => Clipboard.setData(ClipboardData(text: contentHolder.content.decoded)), + ), + ] + ], + ); + } +} + +abstract class PanelContentHolder { + const PanelContentHolder(); +} + +class PanelDataContentHolder extends PanelContentHolder { + final Map content; + + const PanelDataContentHolder({ + required this.content, + }); +} + +class PanelLinkContentHolder extends PanelContentHolder { + final DeepLink content; + + const PanelLinkContentHolder({ + required this.content, + }); +} + +class Data { + final String? value; + final String? tooltip; + final Color? color; + + const Data({ + this.value, + this.tooltip, + this.color, + }); +} diff --git a/pubspec.lock b/pubspec.lock index f31766c8..3381b05a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: cb3a948a1eebbf8efd987c43f95418269930e912a88bc7b6a5a7658423133635 + url: "https://pub.dev" + source: hosted + version: "1.0.17" async: dependency: transitive description: @@ -105,6 +113,46 @@ packages: url: "https://pub.dev" source: hosted version: "5.2.5" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "1c121a478af23755b0b93fd4aa70d3bd32a587dd51ef0a3979091ac0d2317d32" + url: "https://pub.dev" + source: hosted + version: "2.7.1" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: "5615b30c36f55b2777d0533771deda7e5730e769e5d3cb7fda79e9bed86cfa55" + url: "https://pub.dev" + source: hosted + version: "4.5.3" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: "0c1cf1f1022d2245ac117443bb95207952ca770281524d2908e323bc063fb8ff" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + firebase_dynamic_links: + dependency: "direct main" + description: + name: firebase_dynamic_links + sha256: "6eb2ce2793fa68f2749d32ca0210cb355d64deaf3e5b123eca23ca0b7bad92fc" + url: "https://pub.dev" + source: hosted + version: "5.0.16" + firebase_dynamic_links_platform_interface: + dependency: transitive + description: + name: firebase_dynamic_links_platform_interface + sha256: "30470498001d3883d118da43de09e05415200ab668455bfd935943db73f06541" + url: "https://pub.dev" + source: hosted + version: "0.2.3+31" flutter: dependency: "direct main" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index ea998b2c..ef9aae64 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,6 +23,8 @@ dependencies: file_picker: ^5.2.5 font_awesome_flutter: ^10.4.0 sentry_flutter: ^6.20.1 + firebase_dynamic_links: ^5.0.16 + firebase_core: ^2.7.1 dev_dependencies: flutter_test: