diff --git a/.circleci/config.yml b/.circleci/config.yml index c28cb47a4..fb49027d0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,7 +9,7 @@ commands: setup_flutter: steps: - flutter/install_sdk_and_pub: - flutter_version: 3.3.0 + flutter_version: 3.7.3 - run: name: Generate Pigeons command: sh ./scripts/pigeon.sh @@ -80,6 +80,9 @@ jobs: - checkout - node/install-packages: pkg-manager: yarn + override-ci-command: yarn install --frozen-lockfile --network-concurrency 1 + - attach_workspace: + at: coverage - run: name: Run Danger command: yarn danger ci @@ -96,6 +99,13 @@ jobs: - run: sh ./scripts/pigeon.sh - run: flutter pub run build_runner build --delete-conflicting-outputs - run: flutter test --coverage + - run: + working_directory: coverage + command: lcov --remove lcov.info '*.g.dart' '*.mocks.dart' -o lcov.info + - persist_to_workspace: + root: coverage + paths: + - lcov.info test_android: executor: @@ -175,7 +185,7 @@ jobs: - run: flutter pub get - run: name: Check Format - command: flutter format . --set-exit-if-changed + command: dart format . --set-exit-if-changed lint_flutter: docker: @@ -234,7 +244,9 @@ workflows: version: 2 build-test-and-approval-deploy: jobs: - - danger + - danger: + requires: + - test_flutter-stable - test_flutter: name: test_flutter-stable version: stable diff --git a/analysis_options.yaml b/analysis_options.yaml index 9e956095e..ce8381e19 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -2,7 +2,6 @@ include: package:lint/analysis_options_package.yaml analyzer: exclude: - - "example/**" - "**/*.g.dart" linter: diff --git a/build.yaml b/build.yaml new file mode 100644 index 000000000..1a4ea87c5 --- /dev/null +++ b/build.yaml @@ -0,0 +1,6 @@ +targets: + $default: + sources: + # Exclude the 'example' folder from the build + exclude: + - example/** diff --git a/dangerfile.ts b/dangerfile.ts index 5268c527b..670ae0b05 100644 --- a/dangerfile.ts +++ b/dangerfile.ts @@ -1,4 +1,5 @@ import { danger, fail, schedule, warn } from 'danger'; +import collectCoverage, { ReportType } from '@instabug/danger-plugin-coverage'; const hasSourceChanges = danger.git.modified_files.some((file) => file.startsWith('lib/') @@ -27,3 +28,10 @@ async function hasDescription() { } schedule(hasDescription()); + +collectCoverage({ + label: 'Dart', + type: ReportType.LCOV, + filePath: 'coverage/lcov.info', + threshold: 80, +}); diff --git a/example/.gitignore b/example/.gitignore index 9d532b18a..24476c5d1 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -8,6 +8,7 @@ .buildlog/ .history .svn/ +migrate_working_dir/ # IntelliJ related *.iml @@ -31,11 +32,13 @@ .pub/ /build/ -# Web related -lib/generated_plugin_registrant.dart - # 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 index cd984dd00..262ceed06 100644 --- a/example/.metadata +++ b/example/.metadata @@ -1,10 +1,45 @@ # 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. +# This file should be version controlled. version: - revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6 + revision: 135454af32477f815a7525073027a3ff9eff1bfd channel: stable project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 135454af32477f815a7525073027a3ff9eff1bfd + base_revision: 135454af32477f815a7525073027a3ff9eff1bfd + - platform: android + create_revision: 135454af32477f815a7525073027a3ff9eff1bfd + base_revision: 135454af32477f815a7525073027a3ff9eff1bfd + - platform: ios + create_revision: 135454af32477f815a7525073027a3ff9eff1bfd + base_revision: 135454af32477f815a7525073027a3ff9eff1bfd + - platform: linux + create_revision: 135454af32477f815a7525073027a3ff9eff1bfd + base_revision: 135454af32477f815a7525073027a3ff9eff1bfd + - platform: macos + create_revision: 135454af32477f815a7525073027a3ff9eff1bfd + base_revision: 135454af32477f815a7525073027a3ff9eff1bfd + - platform: web + create_revision: 135454af32477f815a7525073027a3ff9eff1bfd + base_revision: 135454af32477f815a7525073027a3ff9eff1bfd + - platform: windows + create_revision: 135454af32477f815a7525073027a3ff9eff1bfd + base_revision: 135454af32477f815a7525073027a3ff9eff1bfd + + # 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/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 000000000..d00720089 --- /dev/null +++ b/example/analysis_options.yaml @@ -0,0 +1,6 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + require_trailing_commas: true + library_private_types_in_public_api: false diff --git a/example/assets/fonts/Axiforma Regular.otf b/example/assets/fonts/Axiforma Regular.otf new file mode 100644 index 000000000..78c55a3c8 Binary files /dev/null and b/example/assets/fonts/Axiforma Regular.otf differ diff --git a/example/assets/images/APM.png b/example/assets/images/APM.png new file mode 100644 index 000000000..bc864c9e2 Binary files /dev/null and b/example/assets/images/APM.png differ diff --git a/example/assets/images/Bug Reporting.png b/example/assets/images/Bug Reporting.png new file mode 100644 index 000000000..c16b8f918 Binary files /dev/null and b/example/assets/images/Bug Reporting.png differ diff --git a/example/assets/images/Core.png b/example/assets/images/Core.png new file mode 100644 index 000000000..4a52210b2 Binary files /dev/null and b/example/assets/images/Core.png differ diff --git a/example/assets/images/Crash Reporting.png b/example/assets/images/Crash Reporting.png new file mode 100644 index 000000000..c6290ff04 Binary files /dev/null and b/example/assets/images/Crash Reporting.png differ diff --git a/example/assets/images/Feature Requests.png b/example/assets/images/Feature Requests.png new file mode 100644 index 000000000..318b00676 Binary files /dev/null and b/example/assets/images/Feature Requests.png differ diff --git a/example/assets/images/Network.png b/example/assets/images/Network.png new file mode 100644 index 000000000..04e0cae2e Binary files /dev/null and b/example/assets/images/Network.png differ diff --git a/example/assets/images/Replies.png b/example/assets/images/Replies.png new file mode 100644 index 000000000..122b6674e Binary files /dev/null and b/example/assets/images/Replies.png differ diff --git a/example/assets/images/Surveys.png b/example/assets/images/Surveys.png new file mode 100644 index 000000000..b4151f725 Binary files /dev/null and b/example/assets/images/Surveys.png differ diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 432b32d6f..097d7cee1 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -444,6 +444,7 @@ }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -475,6 +476,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 808130cd2..4e0eac976 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -47,5 +47,7 @@ Instabug needs access to your photo library so you can attach images. CADisableMinimumFrameDurationOnPhone + UIApplicationSupportsIndirectInputEvents + diff --git a/example/lib/main.dart b/example/lib/main.dart index b42390e9d..428161322 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,13 +1,20 @@ import 'dart:async'; - import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + import 'package:instabug_flutter/instabug_flutter.dart'; -void main() { +import '../models/app_theme.dart'; +import '../providers/bug_reporting_state.dart'; +import '../providers/core_state.dart'; +import '../providers/settings_state.dart'; +import '../screens/main_screen.dart'; + +void main() async { WidgetsFlutterBinding.ensureInitialized(); Instabug.init( - token: 'ed6f659591566da19b67857e1b9d40ab', + token: '0174a800719ebdebf7b248fa6ae2ef17', invocationEvents: [InvocationEvent.floatingButton], debugLogsLevel: LogLevel.verbose, ); @@ -18,361 +25,43 @@ void main() { Zone.current.handleUncaughtError(details.exception, details.stack!); }; - runZonedGuarded(() => runApp(MyApp()), CrashReporting.reportCrash); -} - -class MyApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - navigatorObservers: [ - InstabugNavigatorObserver(), - ], - theme: ThemeData( - primarySwatch: Colors.blue, - visualDensity: VisualDensity.adaptivePlatformDensity, - ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), - ); - } + runZonedGuarded( + () => runApp(const InstabugApp()), + CrashReporting.reportCrash, + ); } -class InstabugButton extends StatelessWidget { - String text; - void Function()? onPressed; - - InstabugButton({required this.text, this.onPressed}); +class InstabugApp extends StatelessWidget { + const InstabugApp({super.key}); @override Widget build(BuildContext context) { - return Container( - width: double.infinity, - margin: const EdgeInsets.only(left: 20.0, right: 20.0), - child: ElevatedButton( - onPressed: onPressed, - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all(Colors.lightBlue), - foregroundColor: MaterialStateProperty.all(Colors.white), + return MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (_) => BugReportingState(), ), - child: Text(text), - ), - ); - } -} - -class InstabugTextField extends StatelessWidget { - String label; - TextEditingController controller; - - InstabugTextField({required this.label, required this.controller}); - - @override - Widget build(BuildContext context) { - return Container( - width: double.infinity, - margin: const EdgeInsets.only(left: 20.0, right: 20.0), - child: TextField( - controller: controller, - decoration: InputDecoration( - labelText: label, + ChangeNotifierProvider( + create: (_) => CoreState(), ), - ), - ); - } -} - -class SectionTitle extends StatelessWidget { - String text; - - SectionTitle(this.text); - - @override - Widget build(BuildContext context) { - return Container( - alignment: Alignment.centerLeft, - margin: const EdgeInsets.only(top: 20.0, left: 20.0), - child: Text( - text, - textAlign: TextAlign.left, - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ), - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({Key? key, required this.title}) : super(key: key); - - final String title; - - @override - _MyHomePageState createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - final buttonStyle = ButtonStyle( - backgroundColor: MaterialStateProperty.all(Colors.lightBlue), - foregroundColor: MaterialStateProperty.all(Colors.white), - ); - - List reportTypes = []; - - final primaryColorController = TextEditingController(); - final screenNameController = TextEditingController(); - - void restartInstabug() { - Instabug.setEnabled(false); - Instabug.setEnabled(true); - BugReporting.setInvocationEvents([InvocationEvent.floatingButton]); - } - - void setOnDismissCallback() { - BugReporting.setOnDismissCallback((dismissType, reportType) { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text('On Dismiss'), - content: Text( - 'onDismiss callback called with $dismissType and $reportType', - ), + ChangeNotifierProvider( + create: (_) => SettingsState(), + ), + ], + child: Consumer( + builder: (context, state, child) { + return MaterialApp( + navigatorObservers: [InstabugNavigatorObserver()], + title: 'Instabug Flutter Example', + themeMode: state.colorTheme == ColorTheme.light + ? ThemeMode.light + : ThemeMode.dark, + theme: AppTheme.light, + darkTheme: AppTheme.dark, + home: const MainScreen(), ); }, - ); - }); - } - - void show() { - Instabug.show(); - } - - void reportScreenChange() { - Instabug.reportScreenChange(screenNameController.text); - } - - void sendBugReport() { - BugReporting.show(ReportType.bug, [InvocationOption.emailFieldOptional]); - } - - void sendFeedback() { - BugReporting.show( - ReportType.feedback, [InvocationOption.emailFieldOptional]); - } - - void askQuestion() { - BugReporting.show( - ReportType.question, [InvocationOption.emailFieldOptional]); - } - - void showNpsSurvey() { - Surveys.showSurvey('pcV_mE2ttqHxT1iqvBxL0w'); - } - - void showManualSurvey() { - Surveys.showSurvey('PMqUZXqarkOR2yGKiENB4w'); - } - - void showFeatureRequests() { - FeatureRequests.show(); - } - - void toggleReportType(ReportType reportType) { - if (reportTypes.contains(reportType)) { - reportTypes.remove(reportType); - } else { - reportTypes.add(reportType); - } - BugReporting.setReportTypes(reportTypes); - } - - void changeFloatingButtonEdge() { - BugReporting.setFloatingButtonEdge(FloatingButtonEdge.left, 200); - } - - void setInvocationEvent(InvocationEvent invocationEvent) { - BugReporting.setInvocationEvents([invocationEvent]); - } - - void changePrimaryColor() { - String text = 'FF' + primaryColorController.text.replaceAll('#', ''); - Color color = Color(int.parse(text, radix: 16)); - Instabug.setPrimaryColor(color); - } - - void setColorTheme(ColorTheme colorTheme) { - Instabug.setColorTheme(colorTheme); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(widget.title)), - body: SingleChildScrollView( - physics: ClampingScrollPhysics(), - padding: const EdgeInsets.only(top: 20.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - margin: const EdgeInsets.only( - left: 20.0, right: 20.0, bottom: 20.0), - child: const Text( - 'Hello Instabug\'s awesome user! The purpose of this application is to show you the different options for customizing the SDK and how easy it is to integrate it to your existing app', - textAlign: TextAlign.center, - ), - ), - InstabugButton( - onPressed: restartInstabug, - text: 'Restart Instabug', - ), - SectionTitle('Primary Color'), - InstabugTextField( - controller: primaryColorController, - label: 'Enter primary color', - ), - InstabugButton( - text: 'Change Primary Color', - onPressed: changePrimaryColor, - ), - SectionTitle('Change Invocation Event'), - ButtonBar( - mainAxisSize: MainAxisSize.min, - alignment: MainAxisAlignment.start, - children: [ - ElevatedButton( - onPressed: () => setInvocationEvent(InvocationEvent.none), - style: buttonStyle, - child: const Text('None'), - ), - ElevatedButton( - onPressed: () => setInvocationEvent(InvocationEvent.shake), - style: buttonStyle, - child: const Text('Shake'), - ), - ElevatedButton( - onPressed: () => - setInvocationEvent(InvocationEvent.screenshot), - style: buttonStyle, - child: const Text('Screenshot'), - ), - ], - ), - ButtonBar( - mainAxisSize: MainAxisSize.min, - alignment: MainAxisAlignment.start, - children: [ - ElevatedButton( - onPressed: () => - setInvocationEvent(InvocationEvent.floatingButton), - style: buttonStyle, - child: const Text('Floating Button'), - ), - ElevatedButton( - onPressed: () => - setInvocationEvent(InvocationEvent.twoFingersSwipeLeft), - style: buttonStyle, - child: const Text('Two Fingers Swipe Left'), - ), - ], - ), - InstabugButton( - onPressed: show, - text: 'Invoke', - ), - InstabugButton( - onPressed: setOnDismissCallback, - text: 'Set On Dismiss Callback', - ), - SectionTitle('Repro Steps'), - InstabugTextField( - controller: screenNameController, - label: 'Enter screen name', - ), - InstabugButton( - text: 'Report Screen Change', - onPressed: reportScreenChange, - ), - InstabugButton( - onPressed: sendBugReport, - text: 'Send Bug Report', - ), - InstabugButton( - onPressed: showManualSurvey, - text: 'Show Manual Survey', - ), - SectionTitle('Change Report Types'), - ButtonBar( - mainAxisSize: MainAxisSize.min, - alignment: MainAxisAlignment.start, - children: [ - ElevatedButton( - onPressed: () => toggleReportType(ReportType.bug), - style: buttonStyle, - child: const Text('Bug'), - ), - ElevatedButton( - onPressed: () => toggleReportType(ReportType.feedback), - style: buttonStyle, - child: const Text('Feedback'), - ), - ElevatedButton( - onPressed: () => toggleReportType(ReportType.question), - style: buttonStyle, - child: const Text('Question'), - ), - ], - ), - InstabugButton( - onPressed: changeFloatingButtonEdge, - text: 'Move Floating Button to Left', - ), - InstabugButton( - onPressed: sendFeedback, - text: 'Send Feedback', - ), - InstabugButton( - onPressed: askQuestion, - text: 'Ask a Question', - ), - InstabugButton( - onPressed: showNpsSurvey, - text: 'Show NPS Survey', - ), - InstabugButton( - onPressed: showManualSurvey, - text: 'Show Multiple Questions Survey', - ), - InstabugButton( - onPressed: showFeatureRequests, - text: 'Show Feature Requests', - ), - SectionTitle('Color Theme'), - ButtonBar( - mainAxisSize: MainAxisSize.max, - alignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - onPressed: () => setColorTheme(ColorTheme.light), - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all(Colors.white), - foregroundColor: - MaterialStateProperty.all(Colors.lightBlue), - ), - child: const Text('Light'), - ), - ElevatedButton( - onPressed: () => setColorTheme(ColorTheme.dark), - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all(Colors.black), - foregroundColor: MaterialStateProperty.all(Colors.white), - ), - child: const Text('Dark'), - ), - ], - ), - ], - )), // This trailing comma makes auto-formatting nicer for build methods. + ), ); } } diff --git a/example/lib/models/app_flow.dart b/example/lib/models/app_flow.dart new file mode 100644 index 000000000..524810b4d --- /dev/null +++ b/example/lib/models/app_flow.dart @@ -0,0 +1,13 @@ +import 'package:flutter/widgets.dart'; + +class AppFlow { + final String title; + final Widget page; + final IconData icon; + + const AppFlow({ + required this.title, + required this.page, + required this.icon, + }); +} diff --git a/example/lib/models/app_theme.dart b/example/lib/models/app_theme.dart new file mode 100644 index 000000000..551d3dfd9 --- /dev/null +++ b/example/lib/models/app_theme.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; + +abstract class AppColors { + static const primary = Color(0xFF00287a); + static const secondary = Color(0xFF5DAAF0); + static const primaryDark = Color(0xFF212121); +} + +class AppTheme { + static final light = ThemeData( + colorScheme: ColorScheme.fromSwatch().copyWith( + brightness: Brightness.light, + primary: AppColors.primary, + secondary: AppColors.secondary, + ), + scaffoldBackgroundColor: Colors.grey[100], + appBarTheme: const AppBarTheme( + color: AppColors.primary, + ), + visualDensity: VisualDensity.adaptivePlatformDensity, + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + backgroundColor: AppColors.primary, + unselectedItemColor: Colors.white, + selectedItemColor: AppColors.secondary, + ), + chipTheme: const ChipThemeData( + selectedColor: AppColors.secondary, + ), + iconTheme: IconThemeData(color: Colors.grey[600]), + textTheme: const TextTheme( + headlineMedium: TextStyle( + fontFamily: 'Axiforma', + fontSize: 16.0, + color: AppColors.primary, + fontWeight: FontWeight.w600, + ), + ), + ); + + static final dark = ThemeData.dark().copyWith( + colorScheme: ColorScheme.fromSwatch().copyWith( + secondary: AppColors.secondary, + ), + appBarTheme: const AppBarTheme( + color: AppColors.primaryDark, + ), + visualDensity: VisualDensity.adaptivePlatformDensity, + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + backgroundColor: AppColors.primaryDark, + ), + chipTheme: const ChipThemeData( + selectedColor: AppColors.secondary, + backgroundColor: Color(0xFFB3B3B3), + ), + iconTheme: const IconThemeData(color: Colors.white), + textTheme: const TextTheme( + headlineMedium: TextStyle( + fontFamily: 'Axiforma', + fontSize: 16.0, + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + ); +} diff --git a/example/lib/models/attachment_type.dart b/example/lib/models/attachment_type.dart new file mode 100644 index 000000000..fe0749296 --- /dev/null +++ b/example/lib/models/attachment_type.dart @@ -0,0 +1,6 @@ +enum AttachmentType { + screenshot, + extraScreenshot, + galleryImage, + screenRecording +} diff --git a/example/lib/models/product_item.dart b/example/lib/models/product_item.dart new file mode 100644 index 000000000..cfe274c7b --- /dev/null +++ b/example/lib/models/product_item.dart @@ -0,0 +1,15 @@ +import 'package:flutter/widgets.dart'; + +class ProductItem { + final String title; + final String imageUrl; + final Color color; + final Widget screen; + + const ProductItem({ + required this.title, + required this.imageUrl, + required this.color, + required this.screen, + }); +} diff --git a/example/lib/providers/bug_reporting_state.dart b/example/lib/providers/bug_reporting_state.dart new file mode 100644 index 000000000..4b1926589 --- /dev/null +++ b/example/lib/providers/bug_reporting_state.dart @@ -0,0 +1,76 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; + +import 'package:instabug_flutter/instabug_flutter.dart'; +import '../models/attachment_type.dart'; + +class BugReportingState with ChangeNotifier { + var _extraAttachments = { + AttachmentType.screenshot, + AttachmentType.extraScreenshot, + AttachmentType.galleryImage, + AttachmentType.screenRecording, + }; + Set get extraAttachments => _extraAttachments; + set extraAttachments(Set attachments) { + _extraAttachments = attachments; + notifyListeners(); + } + + var _invocationOptions = {}; + Set get invocationOptions => _invocationOptions; + set invocationOptions(Set options) { + _invocationOptions = options; + notifyListeners(); + } + + var _invocationEvents = {InvocationEvent.floatingButton}; + Set get invocationEvents => _invocationEvents; + set invocationEvents(Set events) { + _invocationEvents = events; + notifyListeners(); + } + + var _extendedMode = ExtendedBugReportMode.disabled; + ExtendedBugReportMode get extendedMode => _extendedMode; + set extendedMode(ExtendedBugReportMode mode) { + _extendedMode = mode; + notifyListeners(); + } + + var _videoRecordingPosition = Position.bottomRight; + Position get videoRecordingPosition => _videoRecordingPosition; + set videoRecordingPosition(Position position) { + _videoRecordingPosition = position; + notifyListeners(); + } + + var _floatingButtonEdge = FloatingButtonEdge.right; + FloatingButtonEdge get floatingButtonEdge => _floatingButtonEdge; + set floatingButtonEdge(FloatingButtonEdge edge) { + _floatingButtonEdge = edge; + notifyListeners(); + } + + var _disclaimerText = ''; + String get disclaimerText => _disclaimerText; + set disclaimerText(String text) { + _disclaimerText = text; + notifyListeners(); + } + + var _characterCount = ''; + String get characterCount => _characterCount; + set characterCount(String count) { + _characterCount = count; + notifyListeners(); + } + + var _floatingButtonOffset = Platform.isIOS ? 100 : 250; + int get floatingButtonOffset => _floatingButtonOffset; + set floatingButtonOffset(int offset) { + _floatingButtonOffset = offset; + notifyListeners(); + } +} diff --git a/example/lib/providers/core_state.dart b/example/lib/providers/core_state.dart new file mode 100644 index 000000000..c15ea2133 --- /dev/null +++ b/example/lib/providers/core_state.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; + +class CoreState extends ChangeNotifier { + bool _isEnabled = true; + + bool get isEnabled => _isEnabled; + set isEnabled(bool value) { + _isEnabled = value; + notifyListeners(); + } +} diff --git a/example/lib/providers/settings_state.dart b/example/lib/providers/settings_state.dart new file mode 100644 index 000000000..e2a8d2952 --- /dev/null +++ b/example/lib/providers/settings_state.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:instabug_flutter/instabug_flutter.dart'; + +class SettingsState extends ChangeNotifier { + ColorTheme _theme = ColorTheme.light; + // bool _isDarkTheme = false; + // ThemeData _themeData = AppTheme.lightTheme; + + // bool get isDarkTheme => _isDarkTheme; + + ColorTheme get colorTheme => _theme; + void setColorTheme(ColorTheme theme) { + // _isDarkTheme = !_isDarkTheme; + // _themeData = isDarkMode ? AppTheme.dark : AppTheme.light; + _theme = theme; + notifyListeners(); + } + + final Map _colors = { + 'Default': const Color(0xFF1D82DC), + 'Red': Colors.red, + 'Green': Colors.green, + 'Blue': Colors.blue, + 'Yellow': Colors.yellow, + 'Orange': Colors.orange, + 'Purple': Colors.purple, + 'Pink': Colors.pink, + }; + + String _selectedColorName = 'Default'; + + Map get colors => _colors; + + Color get selectedColor => _colors[_selectedColorName]!; + + String get selectedColorName => _selectedColorName; + void selectColor(String colorName) { + _selectedColorName = colorName; + notifyListeners(); + } +} diff --git a/example/lib/screens/apm_screen.dart b/example/lib/screens/apm_screen.dart new file mode 100644 index 000000000..f6a8f43f2 --- /dev/null +++ b/example/lib/screens/apm_screen.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +class APMScreen extends StatelessWidget { + const APMScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text( + 'APM', + ), + ), + ); + } +} diff --git a/example/lib/screens/bug_reporting_screen.dart b/example/lib/screens/bug_reporting_screen.dart new file mode 100644 index 000000000..e60293107 --- /dev/null +++ b/example/lib/screens/bug_reporting_screen.dart @@ -0,0 +1,304 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:instabug_flutter/instabug_flutter.dart'; + +import '../models/attachment_type.dart'; +import '../providers/bug_reporting_state.dart'; +import '../utils/enum_extensions.dart'; +import '../widgets/chip_picker.dart'; +import '../widgets/feature_tile.dart'; +import '../widgets/section_card.dart'; +import '../widgets/separated_list_view.dart'; + +class BugReportingScreen extends StatelessWidget { + const BugReportingScreen({super.key}); + + @override + Widget build(BuildContext context) { + final state = Provider.of(context); + return GestureDetector( + onTap: () { + FocusManager.instance.primaryFocus?.unfocus(); + }, + child: Scaffold( + appBar: AppBar( + title: const Text( + 'Bug Reporting', + ), + ), + body: MediaQuery.removePadding( + context: context, + removeBottom: true, + removeTop: true, + child: SeparatedListView( + padding: const EdgeInsets.all(8.0), + separator: const SizedBox( + height: 8.0, + ), + children: [ + SectionCard( + children: [ + FeatureTile( + leading: const Icon(Icons.bug_report), + title: const Text('Report a bug'), + onTap: () => BugReporting.show( + ReportType.bug, + state.invocationOptions.toList(), + ), + ), + FeatureTile( + leading: const Icon(Icons.feedback), + title: const Text('Suggest an improvement'), + onTap: () => BugReporting.show( + ReportType.feedback, + state.invocationOptions.toList(), + ), + ), + FeatureTile( + leading: const Icon(Icons.question_mark), + title: const Text('Ask a question'), + onTap: () => BugReporting.show( + ReportType.question, + state.invocationOptions.toList(), + ), + ), + ], + ), + SectionCard( + children: [ + FeatureTile( + leading: const Icon(Icons.touch_app), + title: const Text('Invocation Events'), + bottom: ChipPicker( + items: InvocationEvent.values.toSet(), + values: state.invocationEvents, + labelBuilder: (value) => value.capitalizedName, + onChanged: (values) { + state.invocationEvents = values; + BugReporting.setInvocationEvents( + state.invocationEvents.toList(), + ); + }, + ), + ), + FeatureTile( + leading: const Icon(Icons.attachment), + title: const Text('Attachments'), + bottom: ChipPicker( + items: AttachmentType.values.toSet(), + values: state.extraAttachments, + labelBuilder: (value) => value.capitalizedName, + onChanged: (values) { + state.extraAttachments = values; + BugReporting.setEnabledAttachmentTypes( + state.extraAttachments + .contains(AttachmentType.screenshot), + state.extraAttachments + .contains(AttachmentType.extraScreenshot), + state.extraAttachments + .contains(AttachmentType.galleryImage), + state.extraAttachments + .contains(AttachmentType.screenRecording), + ); + }, + ), + ), + FeatureTile( + leading: const Icon(Icons.dynamic_form), + title: const Text('Invocation Options'), + bottom: ChipPicker( + items: InvocationOption.values.toSet(), + values: state.invocationOptions, + labelBuilder: (value) => + value.capitalizedName.replaceAll('Field ', ''), + onChanged: (values) { + state.invocationOptions = values; + BugReporting.setInvocationOptions( + state.invocationOptions.toList(), + ); + }, + ), + ), + Tooltip( + message: + 'Minimum number of character required for the comments field', + child: FeatureTile( + leftFlex: 2, + leading: const Icon(Icons.comment), + title: const Text('Comment Minimum'), + right: Focus( + onFocusChange: (hasFocus) { + if (!hasFocus && state.characterCount.isNotEmpty) { + BugReporting.setCommentMinimumCharacterCount( + int.parse(state.characterCount), + ); + } + }, + child: TextFormField( + initialValue: state.characterCount, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + contentPadding: EdgeInsets.symmetric(vertical: 8.0), + border: OutlineInputBorder(), + hintText: 'Characters', + ), + enableInteractiveSelection: true, + onChanged: (value) => state.characterCount = value, + ), + ), + ), + ), + FeatureTile( + leftFlex: 5, + leading: const Icon(Icons.keyboard_control), + title: const Text('Extended Report'), + rightFlex: 4, + right: DropdownMenu( + width: 150, + initialSelection: state.extendedMode, + label: const Text('Mode'), + dropdownMenuEntries: ExtendedBugReportMode.values + .map>( + (ExtendedBugReportMode mode) { + return DropdownMenuEntry( + value: mode, + label: mode.capitalizedName + .replaceAll('enabledWith', ''), + ); + }).toList(), + onSelected: (ExtendedBugReportMode? mode) { + state.extendedMode = mode!; + BugReporting.setExtendedBugReportMode(mode); + }, + ), + ), + FeatureTile( + leading: const Icon(Icons.info_outline), + title: const Text('Disclaimer Text'), + bottom: TextFormField( + initialValue: state.disclaimerText, + keyboardType: TextInputType.multiline, + textAlign: TextAlign.left, + textAlignVertical: TextAlignVertical.top, + decoration: const InputDecoration( + contentPadding: EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 8.0, + ), + border: OutlineInputBorder(), + ), + enableInteractiveSelection: true, + onFieldSubmitted: (String value) { + state.disclaimerText = value; + BugReporting.setDisclaimerText( + value, + ); + }, + ), + ), + ], + ), + SectionCard( + children: [ + FeatureTile( + leftFlex: 5, + leading: const Icon(Icons.videocam), + title: const Text('Recording Button'), + rightFlex: 4, + right: DropdownMenu( + width: 150, + initialSelection: state.videoRecordingPosition, + label: const Text('Position'), + dropdownMenuEntries: Position.values + .map>( + (Position position) { + return DropdownMenuEntry( + value: position, + label: position.capitalizedName, + ); + }).toList(), + onSelected: (Position? position) { + state.videoRecordingPosition = position!; + BugReporting.setVideoRecordingFloatingButtonPosition( + position, + ); + }, + ), + ), + FeatureTile( + leftFlex: 5, + leading: const Icon(Icons.message), + title: const Text('Floating Button'), + rightFlex: 4, + right: DropdownMenu( + width: 150, + initialSelection: state.floatingButtonEdge, + label: const Text('Edge'), + dropdownMenuEntries: FloatingButtonEdge.values + .map>( + (FloatingButtonEdge edge) { + return DropdownMenuEntry( + value: edge, + label: edge.capitalizedName, + ); + }).toList(), + onSelected: (FloatingButtonEdge? edge) { + state.floatingButtonEdge = edge!; + BugReporting.setFloatingButtonEdge( + edge, + state.floatingButtonOffset, + ); + }, + ), + ), + Tooltip( + message: 'Offset from top', + child: FeatureTile( + leftFlex: 2, + leading: const Icon(Icons.border_top), + title: const Text('Floating Button Offset'), + right: Focus( + onFocusChange: (hasFocus) { + if (!hasFocus) { + BugReporting.setFloatingButtonEdge( + state.floatingButtonEdge, + state.floatingButtonOffset, + ); + } + }, + child: TextFormField( + initialValue: state.floatingButtonOffset.toString(), + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + contentPadding: EdgeInsets.symmetric(vertical: 8.0), + border: OutlineInputBorder(), + ), + enableInteractiveSelection: true, + onChanged: (value) { + if (value.isEmpty) { + state.floatingButtonOffset = + Platform.isIOS ? 100 : 250; + } else { + state.floatingButtonOffset = int.parse(value); + } + }, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/example/lib/screens/core_screen.dart b/example/lib/screens/core_screen.dart new file mode 100644 index 000000000..56974ec82 --- /dev/null +++ b/example/lib/screens/core_screen.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:instabug_flutter/instabug_flutter.dart'; + +import '../providers/core_state.dart'; +import '../widgets/feature_tile.dart'; +import '../widgets/section_card.dart'; +import '../widgets/separated_list_view.dart'; + +class CoreScreen extends StatelessWidget { + const CoreScreen({super.key}); + + @override + Widget build(BuildContext context) { + final state = Provider.of(context); + return Scaffold( + appBar: AppBar( + title: const Text( + 'Core', + ), + ), + body: MediaQuery.removePadding( + context: context, + removeBottom: true, + removeTop: true, + child: SeparatedListView( + padding: const EdgeInsets.all(8.0), + separator: const SizedBox( + height: 8.0, + ), + children: [ + SectionCard( + children: [ + FeatureTile( + leading: const Icon( + Icons.disabled_by_default_outlined, + ), + title: const Text('Disable SDK'), + trailing: Switch( + value: !state.isEnabled, + onChanged: (value) { + Instabug.setEnabled(!value); + state.isEnabled = !value; + }, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/screens/crash_reporting_screen.dart b/example/lib/screens/crash_reporting_screen.dart new file mode 100644 index 000000000..828e0ffe1 --- /dev/null +++ b/example/lib/screens/crash_reporting_screen.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; + +import 'package:instabug_flutter/instabug_flutter.dart'; + +import '../widgets/feature_tile.dart'; +import '../widgets/section_card.dart'; +import '../widgets/separated_list_view.dart'; + +class CrashReportingScreen extends StatelessWidget { + const CrashReportingScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text( + 'Crash Reporting', + ), + ), + body: MediaQuery.removePadding( + context: context, + removeBottom: true, + removeTop: true, + child: SeparatedListView( + padding: const EdgeInsets.all(8.0), + separator: const SizedBox( + height: 8.0, + ), + children: [ + SectionCard( + children: [ + FeatureTile( + leading: const Icon(Icons.warning_amber_outlined), + title: const Text('Handled Crash'), + onTap: () { + try { + throw Exception( + 'This is a handled crash from Instabug Example App', + ); + } catch (error, stacktrace) { + CrashReporting.reportHandledCrash(error, stacktrace); + const snackBar = SnackBar( + content: Text( + 'A handled crash has been successfully reported!', + ), + ); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + }, + ), + FeatureTile( + leading: const Icon(Icons.warning), + title: const Text('Unhandled Crash'), + onTap: () { + try { + final arr = [1, 2]; + arr[2]; + } finally { + const snackBarText = kDebugMode + ? 'Unhandled Crashes will only be reported in release mode and not in debug mode.' + : 'An unhandled crash has been successfully reported!'; + const snackBar = SnackBar( + content: Text(snackBarText), + ); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + }, + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/screens/feature_requests_screen.dart b/example/lib/screens/feature_requests_screen.dart new file mode 100644 index 000000000..51cd4a72b --- /dev/null +++ b/example/lib/screens/feature_requests_screen.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +import 'package:instabug_flutter/instabug_flutter.dart'; + +import '../widgets/feature_tile.dart'; +import '../widgets/section_card.dart'; +import '../widgets/separated_list_view.dart'; + +class FeatureRequestsScreen extends StatelessWidget { + const FeatureRequestsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text( + 'Feature Requests', + ), + ), + body: MediaQuery.removePadding( + context: context, + removeBottom: true, + removeTop: true, + child: SeparatedListView( + padding: const EdgeInsets.all(8.0), + separator: const SizedBox( + height: 8.0, + ), + children: const [ + SectionCard( + children: [ + FeatureTile( + leading: Icon(Icons.lightbulb), + title: Text('Show Feature Requests'), + onTap: FeatureRequests.show, + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/screens/main_screen.dart b/example/lib/screens/main_screen.dart new file mode 100644 index 000000000..fefa7ab47 --- /dev/null +++ b/example/lib/screens/main_screen.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; + +import './products_screen.dart'; +import './settings_screen.dart'; +import '../models/app_flow.dart'; + +class MainScreen extends StatefulWidget { + const MainScreen({super.key}); + + @override + _MainScreenState createState() => _MainScreenState(); +} + +class _MainScreenState extends State { + int selectedPageIndex = 0; + + static const flows = [ + AppFlow( + title: 'Home', + page: ProductsScreen(), + icon: Icons.home, + ), + AppFlow( + title: 'Settings', + page: SettingsScreen(), + icon: Icons.settings, + ), + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(flows[selectedPageIndex].title), + ), + body: IndexedStack( + index: selectedPageIndex, + children: flows.map((flow) => flow.page).toList(), + ), + bottomNavigationBar: BottomNavigationBar( + onTap: (index) { + setState(() { + selectedPageIndex = index; + }); + }, + currentIndex: selectedPageIndex, + items: flows + .map( + (flow) => BottomNavigationBarItem( + icon: Icon(flow.icon), + label: flow.title, + ), + ) + .toList(), + ), + ); + } +} diff --git a/example/lib/screens/network_logger_screen.dart b/example/lib/screens/network_logger_screen.dart new file mode 100644 index 000000000..667d75c65 --- /dev/null +++ b/example/lib/screens/network_logger_screen.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +class NetworkLoggerScreen extends StatelessWidget { + const NetworkLoggerScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text( + 'Network Logger', + ), + ), + ); + } +} diff --git a/example/lib/screens/products_screen.dart b/example/lib/screens/products_screen.dart new file mode 100644 index 000000000..91db6c23e --- /dev/null +++ b/example/lib/screens/products_screen.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; + +import '../models/product_item.dart'; +import '../screens/apm_screen.dart'; +import '../screens/bug_reporting_screen.dart'; +import '../screens/core_screen.dart'; +import '../screens/crash_reporting_screen.dart'; +import '../screens/feature_requests_screen.dart'; +import '../screens/network_logger_screen.dart'; +import '../screens/replies_screen.dart'; +import '../screens/surveys_screen.dart'; +import '../widgets/product_card.dart'; + +class ProductsScreen extends StatelessWidget { + static const products = [ + ProductItem( + title: 'Bug Reporting', + imageUrl: 'assets/images/Bug Reporting.png', + color: Color(0x3000287A), + screen: BugReportingScreen(), + ), + ProductItem( + title: 'Crash Reporting', + imageUrl: 'assets/images/Crash Reporting.png', + color: Color(0x30E91002), + screen: CrashReportingScreen(), + ), + ProductItem( + title: 'APM', + imageUrl: 'assets/images/APM.png', + color: Color(0x30008037), + screen: APMScreen(), + ), + ProductItem( + title: 'Replies', + imageUrl: 'assets/images/Replies.png', + color: Color(0x309D03A0), + screen: RepliesScreen(), + ), + ProductItem( + title: 'Surveys', + imageUrl: 'assets/images/Surveys.png', + color: Color(0x30FF1887), + screen: SurveysScreen(), + ), + ProductItem( + title: 'Feature Requests', + imageUrl: 'assets/images/Feature Requests.png', + color: Color(0x30FFA721), + screen: FeatureRequestsScreen(), + ), + ProductItem( + title: 'Core', + imageUrl: 'assets/images/Core.png', + color: Color(0x305DABF0), + screen: CoreScreen(), + ), + ProductItem( + title: 'Network Logger', + imageUrl: 'assets/images/Network.png', + color: Color(0x30F4CE04), + screen: NetworkLoggerScreen(), + ), + ]; + + const ProductsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: GridView.count( + physics: const ClampingScrollPhysics(), + crossAxisCount: 2, + padding: const EdgeInsets.all(20.0), + crossAxisSpacing: 10, + mainAxisSpacing: 10, + children: products + .map( + (product) => ProductCard( + title: product.title, + imageUrl: product.imageUrl, + color: product.color, + ), + ) + .toList(), + ), + ); + } +} diff --git a/example/lib/screens/replies_screen.dart b/example/lib/screens/replies_screen.dart new file mode 100644 index 000000000..d7fe38aba --- /dev/null +++ b/example/lib/screens/replies_screen.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; + +import 'package:instabug_flutter/instabug_flutter.dart'; + +import '../widgets/feature_tile.dart'; +import '../widgets/section_card.dart'; +import '../widgets/separated_list_view.dart'; + +class RepliesScreen extends StatefulWidget { + const RepliesScreen({super.key}); + + @override + State createState() => _RepliesScreenState(); +} + +class _RepliesScreenState extends State { + var hasChats = false; + var count = 0; + + @override + void initState() { + super.initState(); + callRepliesAsyncAPIs(); + } + + Future callRepliesAsyncAPIs() async { + hasChats = await Replies.hasChats(); + count = await Replies.getUnreadRepliesCount(); + setState(() { + hasChats = hasChats; + count = count; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text( + 'Replies', + ), + ), + body: MediaQuery.removePadding( + context: context, + removeBottom: true, + removeTop: true, + child: SeparatedListView( + padding: const EdgeInsets.all(8.0), + separator: const SizedBox( + height: 8.0, + ), + children: [ + SectionCard( + children: [ + FeatureTile( + leading: const Icon(Icons.chat_bubble), + title: const Text('Show'), + onTap: () => Replies.show(), + ), + FeatureTile( + leading: const Icon(Icons.chat), + title: const Text('Has Chats'), + trailing: hasChats + ? const Icon( + Icons.check, + color: Colors.green, + ) + : const Icon( + Icons.close, + color: Colors.red, + ), + ), + FeatureTile( + leading: const Icon(Icons.mark_chat_unread), + title: const Text('Unread Replies Count'), + trailing: Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text( + count.toString(), + ), + ), + ) + ], + ) + ], + ), + ), + ); + } +} diff --git a/example/lib/screens/settings_screen.dart b/example/lib/screens/settings_screen.dart new file mode 100644 index 000000000..f37644882 --- /dev/null +++ b/example/lib/screens/settings_screen.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:instabug_flutter/instabug_flutter.dart'; + +import '../providers/settings_state.dart'; +import '../widgets/feature_tile.dart'; +import '../widgets/section_card.dart'; +import '../widgets/separated_list_view.dart'; + +class SettingsScreen extends StatelessWidget { + const SettingsScreen({super.key}); + + @override + Widget build(BuildContext context) { + final state = Provider.of(context); + return Scaffold( + body: MediaQuery.removePadding( + context: context, + removeBottom: true, + removeTop: true, + child: SeparatedListView( + padding: const EdgeInsets.all(8.0), + separator: const SizedBox( + height: 8.0, + ), + children: [ + SectionCard( + children: [ + FeatureTile( + leading: Icon( + state.colorTheme == ColorTheme.dark + ? Icons.dark_mode + : Icons.dark_mode_outlined, + ), + title: const Text('Dark Theme'), + trailing: Switch( + value: state.colorTheme == ColorTheme.dark, + onChanged: (value) { + state.setColorTheme( + value ? ColorTheme.dark : ColorTheme.light, + ); + Instabug.setColorTheme(state.colorTheme); + }, + ), + ), + FeatureTile( + leading: const Icon(Icons.palette), + title: const Text('Primary Color'), + bottom: Wrap( + spacing: 4.0, + children: state.colors.keys.map((colorName) { + final color = state.colors[colorName]!; + final isSelected = colorName == state.selectedColorName; + return ChoiceChip( + label: Text(colorName), + labelStyle: TextStyle( + color: isSelected ? Colors.white : Colors.grey[700], + ), + selected: isSelected, + selectedColor: color, + onSelected: (selected) { + if (selected) { + state.selectColor(colorName); + Instabug.setPrimaryColor(color); + } + }, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30.0), + side: isSelected + ? BorderSide.none + : BorderSide( + color: color, + width: 2, + ), + ), + ); + }).toList(), + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/screens/surveys_screen.dart b/example/lib/screens/surveys_screen.dart new file mode 100644 index 000000000..3f8fe6de6 --- /dev/null +++ b/example/lib/screens/surveys_screen.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; + +import 'package:instabug_flutter/instabug_flutter.dart'; + +import '../widgets/feature_tile.dart'; +import '../widgets/section_card.dart'; +import '../widgets/separated_list_view.dart'; + +class SurveysScreen extends StatefulWidget { + const SurveysScreen({super.key}); + + @override + State createState() => _SurveysScreenState(); +} + +class _SurveysScreenState extends State { + final surveyTokenController = TextEditingController(); + + @override + void dispose() { + surveyTokenController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text( + 'Surveys', + ), + ), + body: MediaQuery.removePadding( + context: context, + removeBottom: true, + removeTop: true, + child: SeparatedListView( + padding: const EdgeInsets.all(8.0), + separator: const SizedBox( + height: 8.0, + ), + children: [ + SectionCard( + children: [ + FeatureTile( + leading: const Icon(Icons.rate_review), + title: const Text('Show if available'), + onTap: () { + Surveys.showSurveyIfAvailable(); + }, + ), + FeatureTile( + leading: const Icon(Icons.generating_tokens), + title: const Text('Manual Survey'), + bottom: TextField( + controller: surveyTokenController, + keyboardType: TextInputType.multiline, + textAlign: TextAlign.left, + textAlignVertical: TextAlignVertical.top, + decoration: const InputDecoration( + contentPadding: EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 8.0, + ), + border: OutlineInputBorder(), + ), + onSubmitted: (String value) => + Surveys.showSurvey(surveyTokenController.text), + enableInteractiveSelection: true, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/utils/enum_extensions.dart b/example/lib/utils/enum_extensions.dart new file mode 100644 index 000000000..90ba43b24 --- /dev/null +++ b/example/lib/utils/enum_extensions.dart @@ -0,0 +1,14 @@ +extension EnumLabel on Enum { + String get capitalizedName { + return name + .replaceAllMapped( + RegExp('([A-Z]+)'), + (match) => ' ${match.group(0)}', + ) + .toLowerCase() + .replaceAllMapped( + RegExp('(^|\\s)[a-z]'), + (match) => match.group(0)!.toUpperCase(), + ); + } +} diff --git a/example/lib/widgets/chip_picker.dart b/example/lib/widgets/chip_picker.dart new file mode 100644 index 000000000..cb43ac6c9 --- /dev/null +++ b/example/lib/widgets/chip_picker.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +typedef LabelBuilder = String Function(T value); + +class ChipPicker extends StatelessWidget { + final LabelBuilder labelBuilder; + final Set items; + final Set values; + final ValueChanged> onChanged; + + const ChipPicker({ + super.key, + required this.labelBuilder, + required this.items, + required this.values, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 4.0, + children: items + .map( + (item) => FilterChip( + label: Text( + labelBuilder(item), + ), + selected: values.contains(item), + onSelected: (selected) { + if (selected) { + values.add(item); + } else { + values.remove(item); + } + onChanged(values); + }, + ), + ) + .toList(), + ); + } +} diff --git a/example/lib/widgets/feature_tile.dart b/example/lib/widgets/feature_tile.dart new file mode 100644 index 000000000..0eeb69ccd --- /dev/null +++ b/example/lib/widgets/feature_tile.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; + +class FeatureTile extends StatelessWidget { + final Widget leading; + final Widget title; + final Function? onTap; + final Widget? right; + final Widget? bottom; + final int? leftFlex; + final int? rightFlex; + final Widget? trailing; + + const FeatureTile({ + super.key, + required this.leading, + required this.title, + this.onTap, + this.right, + this.bottom, + this.leftFlex, + this.rightFlex, + this.trailing, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + children: [ + Expanded( + flex: leftFlex ?? 1, + child: ListTile( + leading: IconTheme( + data: Theme.of(context).iconTheme, + child: leading, + ), + title: title, + trailing: trailing, + onTap: onTap != null ? () => onTap!() : null, + ), + ), + if (right != null) + Expanded( + flex: rightFlex ?? 1, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: right!, + ), + ), + ], + ), + if (bottom != null) + Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0, bottom: 8.0), + child: bottom!, + ) + ], + ); + } +} diff --git a/example/lib/widgets/product_card.dart b/example/lib/widgets/product_card.dart new file mode 100644 index 000000000..75225a36f --- /dev/null +++ b/example/lib/widgets/product_card.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; + +import '../models/product_item.dart'; +import '../screens/products_screen.dart'; + +class ProductCard extends StatelessWidget { + final String title; + final String imageUrl; + final Color color; + + const ProductCard({ + super.key, + required this.title, + required this.imageUrl, + required this.color, + }); + + void navToSelectedProduct(BuildContext context) { + ProductItem selectedProduct = + ProductsScreen.products.firstWhere((product) => product.title == title); + + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => selectedProduct.screen, + settings: RouteSettings(name: selectedProduct.title), + ), + ); + } + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () => navToSelectedProduct(context), + splashColor: Theme.of(context).splashColor, + child: Ink( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20.0), + color: Theme.of(context).cardColor, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 80.0, + height: 80.0, + margin: const EdgeInsets.only(top: 20.0), + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + image: DecorationImage( + image: AssetImage(imageUrl), + ), + ), + ), + Expanded( + child: Center( + child: Text( + title, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headlineMedium, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/widgets/section_card.dart b/example/lib/widgets/section_card.dart new file mode 100644 index 000000000..3db4a1663 --- /dev/null +++ b/example/lib/widgets/section_card.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +import '../widgets/separated_list_view.dart'; + +class SectionCard extends StatelessWidget { + final List children; + + const SectionCard({ + super.key, + required this.children, + }); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: Theme.of(context).cardColor, + ), + child: SeparatedListView( + separator: const Divider( + height: 4.0, + thickness: 1.0, + ), + primary: false, + shrinkWrap: true, + children: children, + ), + ); + } +} diff --git a/example/lib/widgets/separated_list_view.dart b/example/lib/widgets/separated_list_view.dart new file mode 100644 index 000000000..45346b950 --- /dev/null +++ b/example/lib/widgets/separated_list_view.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class SeparatedListView extends ListView { + final List children; + final Widget separator; + + SeparatedListView({ + super.key, + super.padding, + super.primary, + super.shrinkWrap, + super.physics = const ClampingScrollPhysics(), + required this.children, + required this.separator, + }) : super.separated( + itemBuilder: (_, index) => children[index], + separatorBuilder: (_, __) => separator, + itemCount: children.length, + ); +} diff --git a/example/pubspec.lock b/example/pubspec.lock index c042872aa..d0e368637 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,82 +5,72 @@ packages: dependency: transitive description: name: archive - sha256: "80e5141fafcb3361653ce308776cfd7d45e6e9fbb429e14eec571382c0c5fecb" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "3.3.2" + version: "3.3.0" async: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "2.10.0" + version: "2.9.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.0" characters: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "1.2.1" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "1.1.1" collection: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.17.0" + version: "1.16.0" crypto: dependency: transitive description: name: crypto - sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "3.0.2" espresso: dependency: "direct dev" description: name: espresso - sha256: "641bdfcaec98b2fe2f5c90d61a16cdf6879ddac4d7333a6467ef03d60933596b" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "0.2.0+5" fake_async: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" file: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "6.1.4" + version: "6.1.2" flutter: dependency: "direct main" description: flutter @@ -91,6 +81,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" + source: hosted + version: "2.0.1" flutter_test: dependency: "direct dev" description: flutter @@ -116,54 +114,72 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.5" + lints: + dependency: transitive + description: + name: lints + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" + source: hosted + version: "2.0.1" matcher: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "0.12.13" + version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "0.1.5" meta: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "1.8.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" path: dependency: transitive description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "1.8.2" platform: dependency: transitive description: name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" process: dependency: transitive description: name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "4.2.4" + provider: + dependency: "direct main" + description: + name: provider + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + url: "https://pub.dev" + source: hosted + version: "6.0.5" sky_engine: dependency: transitive description: flutter @@ -173,90 +189,79 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.9.1" + version: "1.9.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.11.0" + version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.1.1" sync_http: dependency: transitive description: name: sync_http - sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "0.3.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "1.2.1" test_api: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "0.4.16" + version: "0.4.12" typed_data: dependency: transitive description: name: typed_data - sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" + version: "2.1.2" vm_service: dependency: transitive description: name: vm_service - sha256: e7fb6c2282f7631712b69c19d1bff82f3767eea33a2321c14fa59ad67ea391c7 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "9.4.0" + version: "9.0.0" webdriver: dependency: transitive description: name: webdriver - sha256: ef67178f0cc7e32c1494645b11639dd1335f1d18814aa8435113a92e9ef9d841 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.0.0" sdks: - dart: ">=2.18.0 <3.0.0" + dart: ">=2.17.0-0 <3.0.0" flutter: ">=2.10.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index ab9ee0577..afae2f92f 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -18,11 +18,12 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.18.0 <3.0.0" dependencies: flutter: sdk: flutter + provider: ^6.0.5 instabug_flutter: path: ../ @@ -32,6 +33,7 @@ dev_dependencies: sdk: flutter flutter_test: sdk: flutter + flutter_lints: ^2.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec @@ -42,6 +44,12 @@ flutter: # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true + assets: + - assets/images/ + fonts: + - family: Axiforma + fonts: + - asset: assets/fonts/Axiforma Regular.otf # To add assets to your application, add an assets section, like this: # assets: diff --git a/package.json b/package.json index d428bf57e..52b3a844e 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,8 @@ "name": "Instabug-Flutter", "version": "0.0.0", "devDependencies": { - "danger": "^11.2.5" + "@instabug/danger-plugin-coverage": "Instabug/danger-plugin-coverage", + "danger": "^11.2.5", + "typescript": "^5.0.4" } } diff --git a/scripts/pigeon.sh b/scripts/pigeon.sh index 4c948fe20..decc2eb21 100644 --- a/scripts/pigeon.sh +++ b/scripts/pigeon.sh @@ -26,7 +26,7 @@ generate_pigeon() { # Generated files are not formatted by default, # this affects pacakge score. - flutter format "$DIR_DART/$name_snake.api.g.dart" + dart format "$DIR_DART/$name_snake.api.g.dart" } for file in pigeons/** diff --git a/yarn.lock b/yarn.lock index ffcc34e06..d86ee57b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -32,6 +32,12 @@ query-string "^6.12.1" xcase "^2.0.1" +"@instabug/danger-plugin-coverage@Instabug/danger-plugin-coverage": + version "0.0.0-development" + resolved "git+ssh://git@github.com/Instabug/danger-plugin-coverage.git#a3941bd25421b0978ec636648a557b2280d0c9e6" + dependencies: + fast-xml-parser "^4.2.0" + "@octokit/auth-token@^2.4.4": version "2.5.0" resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.5.0.tgz#27c37ea26c205f28443402477ffd261311f21e36" @@ -410,6 +416,13 @@ fast-json-patch@^3.0.0-1: resolved "https://registry.yarnpkg.com/fast-json-patch/-/fast-json-patch-3.1.1.tgz#85064ea1b1ebf97a3f7ad01e23f9337e72c66947" integrity sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ== +fast-xml-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.2.0.tgz#6db2ba33b95b8b4af93f94fe024d4b4d02a50855" + integrity sha512-+zVQv4aVTO+o8oRUyRL7PjgeVo1J6oP8Cw2+a8UTZQcj5V0yUK5T63gTN0ldgiHDPghUjKc4OpT6SwMTwnOQug== + dependencies: + strnum "^1.0.5" + fill-range@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -871,6 +884,11 @@ strict-uri-encode@^2.0.0: resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ== +strnum@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" + integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA== + supports-color@^5.0.0, supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -898,6 +916,11 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== +typescript@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b" + integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== + universal-user-agent@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee"