diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a78c7d0..b595079 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,7 +15,7 @@ jobs: - name: Install & set Flutter version uses: subosito/flutter-action@v2 with: - flutter-version: '3.10.4' + flutter-version: '3.19.6' channel: 'stable' - name: Get package run: flutter pub get diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..7e7e7f6 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1 @@ +extensions: diff --git a/dist/appcast.xml b/dist/appcast.xml index 791fab8..45c3ff4 100644 --- a/dist/appcast.xml +++ b/dist/appcast.xml @@ -5,29 +5,28 @@ en Dipantau - Version 1.5.0 + Version 1.6.0 Fitur

Perbaikan

]]>
- 10 - 1.5.0 + 11 + 1.6.0 - Mon, 07 Oct 2023 09:00:00 +0700 + Sun, 26 Nov 2023 22:00:00 +0700
diff --git a/lib/core/network/network_info.dart b/lib/core/network/network_info.dart index 2223aa1..e004b87 100644 --- a/lib/core/network/network_info.dart +++ b/lib/core/network/network_info.dart @@ -12,6 +12,12 @@ class NetworkInfoImpl implements NetworkInfo { @override Future get isConnected async { final connectivityResult = await connectivity.checkConnectivity(); - return connectivityResult != ConnectivityResult.none; + if (connectivityResult.contains(ConnectivityResult.none)) { + return false; + } else if (connectivityResult.isEmpty) { + return false; + } else { + return true; + } } } diff --git a/lib/core/util/helper.dart b/lib/core/util/helper.dart index 78f8d38..a9eab91 100644 --- a/lib/core/util/helper.dart +++ b/lib/core/util/helper.dart @@ -101,4 +101,8 @@ class Helper { return ConstantErrorMessage().failureUnknown; } } + + String removeTrailingSlash(String input) { + return input.replaceAll(RegExp(r'/+$'), ''); + } } diff --git a/lib/feature/data/datasource/general/general_remote_data_source.dart b/lib/feature/data/datasource/general/general_remote_data_source.dart new file mode 100644 index 0000000..0a98915 --- /dev/null +++ b/lib/feature/data/datasource/general/general_remote_data_source.dart @@ -0,0 +1,36 @@ +import 'package:dio/dio.dart'; +import 'package:dipantau_desktop_client/config/flavor_config.dart'; +import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; + +abstract class GeneralRemoteDataSource { + /// Panggil endpoint [host]/api/ping + /// + /// Throws [DioException] untuk semua error kode + late String pathPing; + + Future ping(String baseUrl); +} + +class GeneralRemoteDataSourceImpl implements GeneralRemoteDataSource { + final Dio dio; + + GeneralRemoteDataSourceImpl({ + required this.dio, + }); + + final baseUrl = FlavorConfig.instance.values.baseUrl; + + @override + String pathPing = ''; + + @override + Future ping(String baseUrl) async { + pathPing = '$baseUrl/api/ping'; + final response = await dio.get(pathPing); + if (response.statusCode.toString().startsWith('2')) { + return GeneralResponse.fromJson(response.data); + } else { + throw DioException(requestOptions: RequestOptions(path: pathPing)); + } + } +} diff --git a/lib/feature/data/repository/general/general_repository_impl.dart b/lib/feature/data/repository/general/general_repository_impl.dart new file mode 100644 index 0000000..be2ad2e --- /dev/null +++ b/lib/feature/data/repository/general/general_repository_impl.dart @@ -0,0 +1,56 @@ +import 'package:dio/dio.dart'; +import 'package:dipantau_desktop_client/core/error/failure.dart'; +import 'package:dipantau_desktop_client/core/network/network_info.dart'; +import 'package:dipantau_desktop_client/feature/data/datasource/general/general_remote_data_source.dart'; +import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; +import 'package:dipantau_desktop_client/feature/domain/repository/general/general_repository.dart'; + +class GeneralRepositoryImpl implements GeneralRepository { + final GeneralRemoteDataSource remoteDataSource; + final NetworkInfo networkInfo; + + GeneralRepositoryImpl({ + required this.remoteDataSource, + required this.networkInfo, + }); + + String getErrorMessageFromEndpoint(dynamic dynamicErrorMessage, String httpErrorMessage, int? statusCode) { + if (dynamicErrorMessage is Map && dynamicErrorMessage.containsKey('message')) { + return '$statusCode ${dynamicErrorMessage['message']}'; + } else if (dynamicErrorMessage is String) { + return httpErrorMessage; + } else { + return httpErrorMessage; + } + } + + @override + Future<({Failure? failure, GeneralResponse? response})> ping(String baseUrl) async { + Failure? failure; + GeneralResponse? response; + final isConnected = await networkInfo.isConnected; + if (isConnected) { + try { + response = await remoteDataSource.ping(baseUrl); + } on DioException catch (error) { + final message = error.message ?? error.toString(); + if (error.response == null) { + failure = ServerFailure(message); + } else { + final errorMessage = getErrorMessageFromEndpoint( + error.response?.data, + message, + error.response?.statusCode, + ); + failure = ServerFailure(errorMessage); + } + } on TypeError catch (error) { + final errorMessage = error.toString(); + failure = ParsingFailure(errorMessage); + } + } else { + failure = ConnectionFailure(); + } + return (failure: failure, response: response); + } +} diff --git a/lib/feature/domain/repository/general/general_repository.dart b/lib/feature/domain/repository/general/general_repository.dart new file mode 100644 index 0000000..3b26c36 --- /dev/null +++ b/lib/feature/domain/repository/general/general_repository.dart @@ -0,0 +1,6 @@ +import 'package:dipantau_desktop_client/core/error/failure.dart'; +import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; + +abstract class GeneralRepository { + Future<({Failure? failure, GeneralResponse? response})> ping(String baseUrl); +} \ No newline at end of file diff --git a/lib/feature/domain/usecase/ping/ping.dart b/lib/feature/domain/usecase/ping/ping.dart new file mode 100644 index 0000000..3c28a00 --- /dev/null +++ b/lib/feature/domain/usecase/ping/ping.dart @@ -0,0 +1,34 @@ +import 'package:dipantau_desktop_client/core/error/failure.dart'; +import 'package:dipantau_desktop_client/core/usecase/usecase.dart'; +import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; +import 'package:dipantau_desktop_client/feature/domain/repository/general/general_repository.dart'; +import 'package:equatable/equatable.dart'; + +class Ping implements UseCaseRecords { + final GeneralRepository repository; + + Ping({required this.repository}); + + @override + Future<({Failure? failure, GeneralResponse? response})> call(ParamsPing params) { + return repository.ping(params.baseUrl); + } +} + +class ParamsPing extends Equatable { + final String baseUrl; + + ParamsPing({ + required this.baseUrl, + }); + + @override + List get props => [ + baseUrl, + ]; + + @override + String toString() { + return 'ParamsPing{baseUrl: $baseUrl}'; + } +} diff --git a/lib/feature/presentation/bloc/setup_credential/setup_credential_bloc.dart b/lib/feature/presentation/bloc/setup_credential/setup_credential_bloc.dart new file mode 100644 index 0000000..e089437 --- /dev/null +++ b/lib/feature/presentation/bloc/setup_credential/setup_credential_bloc.dart @@ -0,0 +1,43 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:dipantau_desktop_client/core/util/helper.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/ping/ping.dart'; + +part 'setup_credential_event.dart'; + +part 'setup_credential_state.dart'; + +class SetupCredentialBloc extends Bloc { + final Helper helper; + final Ping ping; + + SetupCredentialBloc({ + required this.helper, + required this.ping, + }) : super(InitialSetupCredentialState()) { + on(_onPingSetupCredentialEvent); + } + + FutureOr _onPingSetupCredentialEvent( + PingSetupCredentialEvent event, + Emitter emit, + ) async { + final baseUrl = event.baseUrl; + emit(LoadingSetupCredentialState()); + final result = await ping( + ParamsPing( + baseUrl: baseUrl, + ), + ); + final response = result.response; + final failure = result.failure; + if (response != null) { + emit(SuccessPingSetupCredentialState(baseUrl: baseUrl)); + return; + } + + final errorMessage = helper.getErrorMessageFromFailure(failure); + emit(FailureSetupCredentialState(errorMessage: errorMessage)); + } +} diff --git a/lib/feature/presentation/bloc/setup_credential/setup_credential_event.dart b/lib/feature/presentation/bloc/setup_credential/setup_credential_event.dart new file mode 100644 index 0000000..07219b2 --- /dev/null +++ b/lib/feature/presentation/bloc/setup_credential/setup_credential_event.dart @@ -0,0 +1,16 @@ +part of 'setup_credential_bloc.dart'; + +abstract class SetupCredentialEvent {} + +class PingSetupCredentialEvent extends SetupCredentialEvent { + final String baseUrl; + + PingSetupCredentialEvent({ + required this.baseUrl, + }); + + @override + String toString() { + return 'PingSetupCredentialEvent{baseUrl: $baseUrl}'; + } +} diff --git a/lib/feature/presentation/bloc/setup_credential/setup_credential_state.dart b/lib/feature/presentation/bloc/setup_credential/setup_credential_state.dart new file mode 100644 index 0000000..f2127f6 --- /dev/null +++ b/lib/feature/presentation/bloc/setup_credential/setup_credential_state.dart @@ -0,0 +1,33 @@ +part of 'setup_credential_bloc.dart'; + +abstract class SetupCredentialState {} + +class InitialSetupCredentialState extends SetupCredentialState {} + +class LoadingSetupCredentialState extends SetupCredentialState {} + +class FailureSetupCredentialState extends SetupCredentialState { + final String errorMessage; + + FailureSetupCredentialState({ + required this.errorMessage, + }); + + @override + String toString() { + return 'FailureSetupCredentialState{errorMessage: $errorMessage}'; + } +} + +class SuccessPingSetupCredentialState extends SetupCredentialState { + final String baseUrl; + + SuccessPingSetupCredentialState({ + required this.baseUrl, + }); + + @override + String toString() { + return 'SuccessPingSetupCredentialState{baseUrl: $baseUrl}'; + } +} diff --git a/lib/feature/presentation/page/add_member/add_edit_member_page.dart b/lib/feature/presentation/page/add_member/add_edit_member_page.dart index d56c50e..f9383df 100644 --- a/lib/feature/presentation/page/add_member/add_edit_member_page.dart +++ b/lib/feature/presentation/page/add_member/add_edit_member_page.dart @@ -24,9 +24,9 @@ class AddEditMemberPage extends StatefulWidget { final UserProfileResponse? defaultValue; const AddEditMemberPage({ - Key? key, + super.key, this.defaultValue, - }) : super(key: key); + }); @override State createState() => _AddEditMemberPageState(); diff --git a/lib/feature/presentation/page/edit_profile/edit_profile_page.dart b/lib/feature/presentation/page/edit_profile/edit_profile_page.dart index 4178a14..83a26d9 100644 --- a/lib/feature/presentation/page/edit_profile/edit_profile_page.dart +++ b/lib/feature/presentation/page/edit_profile/edit_profile_page.dart @@ -22,7 +22,7 @@ class EditProfilePage extends StatefulWidget { static const routePath = '/edit-profile'; static const routeName = 'edit-profile'; - const EditProfilePage({Key? key}) : super(key: key); + const EditProfilePage({super.key}); @override State createState() => _EditProfilePageState(); diff --git a/lib/feature/presentation/page/error/error_page.dart b/lib/feature/presentation/page/error/error_page.dart index 1e1c4c8..74b5713 100644 --- a/lib/feature/presentation/page/error/error_page.dart +++ b/lib/feature/presentation/page/error/error_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; class ErrorPage extends StatelessWidget { - const ErrorPage({Key? key}) : super(key: key); + const ErrorPage({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/feature/presentation/page/forgot_password/forgot_password_page.dart b/lib/feature/presentation/page/forgot_password/forgot_password_page.dart index 731d4ae..f982afe 100644 --- a/lib/feature/presentation/page/forgot_password/forgot_password_page.dart +++ b/lib/feature/presentation/page/forgot_password/forgot_password_page.dart @@ -19,9 +19,9 @@ class ForgotPasswordPage extends StatefulWidget { final String? email; const ForgotPasswordPage({ - Key? key, + super.key, required this.email, - }) : super(key: key); + }); @override State createState() => _ForgotPasswordPageState(); diff --git a/lib/feature/presentation/page/home/home_page.dart b/lib/feature/presentation/page/home/home_page.dart index 8fb78c9..ce38b18 100644 --- a/lib/feature/presentation/page/home/home_page.dart +++ b/lib/feature/presentation/page/home/home_page.dart @@ -51,7 +51,7 @@ class HomePage extends StatefulWidget { static const routePath = '/home'; static const routeName = 'home'; - const HomePage({Key? key}) : super(key: key); + const HomePage({super.key}); @override State createState() => _HomePageState(); @@ -129,7 +129,9 @@ class _HomePageState extends State with TrayListener, WindowListener { final appDatabase = await sl.getAsync(); trackDao = appDatabase.trackDao; } catch (error) { - widgetHelper.showSnackBar(context, 'error: $error'); + if (mounted) { + widgetHelper.showSnackBar(context, 'error: $error'); + } } setupCronTimer(); doLoadDataTask(); @@ -644,7 +646,7 @@ class _HomePageState extends State with TrayListener, WindowListener { onTap: () async { final isPermissionScreenRecordingGranted = await platformChannelHelper.checkPermissionScreenRecording(); - if (mounted && isPermissionScreenRecordingGranted != null && !isPermissionScreenRecordingGranted) { + if (context.mounted && isPermissionScreenRecordingGranted != null && !isPermissionScreenRecordingGranted) { widgetHelper.showDialogPermissionScreenRecording(context); return; } @@ -652,7 +654,7 @@ class _HomePageState extends State with TrayListener, WindowListener { if (isPermissionScreenRecordingGranted!) { final isPermissionAccessibilityGranted = await platformChannelHelper.checkPermissionAccessibility(); - if (mounted && isPermissionAccessibilityGranted != null && !isPermissionAccessibilityGranted) { + if (context.mounted && isPermissionAccessibilityGranted != null && !isPermissionAccessibilityGranted) { widgetHelper.showDialogPermissionAccessibility(context); return; } diff --git a/lib/feature/presentation/page/login/login_page.dart b/lib/feature/presentation/page/login/login_page.dart index 52a2693..3518291 100644 --- a/lib/feature/presentation/page/login/login_page.dart +++ b/lib/feature/presentation/page/login/login_page.dart @@ -17,7 +17,7 @@ class LoginPage extends StatefulWidget { static const routePath = '/login'; static const routeName = 'login'; - const LoginPage({Key? key}) : super(key: key); + const LoginPage({super.key}); @override State createState() => _LoginPageState(); @@ -122,7 +122,7 @@ class _LoginPageState extends State { final email = controllerEmail.text.trim(); context.pushNamed( ForgotPasswordPage.routeName, - queryParams: { + queryParameters: { ForgotPasswordPage.parameterEmail: email, }, ); diff --git a/lib/feature/presentation/page/manual_tracking/manual_tracking_page.dart b/lib/feature/presentation/page/manual_tracking/manual_tracking_page.dart index 43b0679..7792e40 100644 --- a/lib/feature/presentation/page/manual_tracking/manual_tracking_page.dart +++ b/lib/feature/presentation/page/manual_tracking/manual_tracking_page.dart @@ -18,7 +18,7 @@ class ManualTrackingPage extends StatefulWidget { static const routePath = '/manual-tracking'; static const routeName = 'manual-tracking'; - const ManualTrackingPage({Key? key}) : super(key: key); + const ManualTrackingPage({super.key}); @override State createState() => _ManualTrackingPageState(); diff --git a/lib/feature/presentation/page/member_setting/member_setting_page.dart b/lib/feature/presentation/page/member_setting/member_setting_page.dart index 973a817..35dbe88 100644 --- a/lib/feature/presentation/page/member_setting/member_setting_page.dart +++ b/lib/feature/presentation/page/member_setting/member_setting_page.dart @@ -18,7 +18,7 @@ class MemberSettingPage extends StatefulWidget { static const routePath = '/member-setting'; static const routeName = 'member-setting'; - const MemberSettingPage({Key? key}) : super(key: key); + const MemberSettingPage({super.key}); @override State createState() => _MemberSettingPageState(); diff --git a/lib/feature/presentation/page/photo_view/photo_view_page.dart b/lib/feature/presentation/page/photo_view/photo_view_page.dart index ad6b667..e2a3486 100644 --- a/lib/feature/presentation/page/photo_view/photo_view_page.dart +++ b/lib/feature/presentation/page/photo_view/photo_view_page.dart @@ -24,10 +24,10 @@ class PhotoViewPage extends StatefulWidget { final bool? isShowIconDownload; PhotoViewPage({ - Key? key, + super.key, required this.listPhotos, required this.isShowIconDownload, - }) : super(key: key); + }); @override State createState() => _PhotoViewPageState(); diff --git a/lib/feature/presentation/page/register/register_page.dart b/lib/feature/presentation/page/register/register_page.dart index fe6fe0c..0048d95 100644 --- a/lib/feature/presentation/page/register/register_page.dart +++ b/lib/feature/presentation/page/register/register_page.dart @@ -17,7 +17,7 @@ class RegisterPage extends StatefulWidget { static const routePath = '/register'; static const routeName = 'register'; - const RegisterPage({Key? key}) : super(key: key); + const RegisterPage({super.key}); @override State createState() => _RegisterPageState(); @@ -51,7 +51,7 @@ class _RegisterPageState extends State { } else if (state is SuccessSubmitSignUpState) { context.goNamed( RegisterSuccessPage.routeName, - queryParams: { + queryParameters: { 'email': state.response.email ?? '-', }, ); diff --git a/lib/feature/presentation/page/register_success/register_success_page.dart b/lib/feature/presentation/page/register_success/register_success_page.dart index 1b1dfe0..b6d2ad4 100644 --- a/lib/feature/presentation/page/register_success/register_success_page.dart +++ b/lib/feature/presentation/page/register_success/register_success_page.dart @@ -14,9 +14,9 @@ class RegisterSuccessPage extends StatelessWidget { final String email; RegisterSuccessPage({ - Key? key, + super.key, required this.email, - }) : super(key: key); + }); final helper = sl(); diff --git a/lib/feature/presentation/page/report_screenshot/report_screenshot_page.dart b/lib/feature/presentation/page/report_screenshot/report_screenshot_page.dart index 4df09c9..e7be2f1 100644 --- a/lib/feature/presentation/page/report_screenshot/report_screenshot_page.dart +++ b/lib/feature/presentation/page/report_screenshot/report_screenshot_page.dart @@ -27,7 +27,7 @@ class ReportScreenshotPage extends StatefulWidget { static const routePath = '/report-screenshot'; static const routeName = 'report-screenshot'; - const ReportScreenshotPage({Key? key}) : super(key: key); + const ReportScreenshotPage({super.key}); @override State createState() => _ReportScreenshotPageState(); @@ -91,10 +91,13 @@ class _ReportScreenshotPageState extends State { @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { + return PopScope( + canPop: false, + onPopInvoked: (didPop) { + if (didPop) { + return; + } context.pop(isRefreshPreviousPage); - return false; }, child: GestureDetector( onTap: () => widgetHelper.unfocus(context), diff --git a/lib/feature/presentation/page/reset_password/reset_password_page.dart b/lib/feature/presentation/page/reset_password/reset_password_page.dart index 1a04511..f5d48d2 100644 --- a/lib/feature/presentation/page/reset_password/reset_password_page.dart +++ b/lib/feature/presentation/page/reset_password/reset_password_page.dart @@ -22,9 +22,9 @@ class ResetPasswordPage extends StatefulWidget { final String code; const ResetPasswordPage({ - Key? key, + super.key, required this.code, - }) : super(key: key); + }); @override State createState() => _ResetPasswordPageState(); diff --git a/lib/feature/presentation/page/reset_password_success/reset_password_success_page.dart b/lib/feature/presentation/page/reset_password_success/reset_password_success_page.dart index aadb3d5..01fff87 100644 --- a/lib/feature/presentation/page/reset_password_success/reset_password_success_page.dart +++ b/lib/feature/presentation/page/reset_password_success/reset_password_success_page.dart @@ -12,8 +12,8 @@ class ResetPasswordSuccessPage extends StatelessWidget { static const routeName = 'reset-password-success'; ResetPasswordSuccessPage({ - Key? key, - }) : super(key: key); + super.key, + }); final helper = sl(); diff --git a/lib/feature/presentation/page/setting/setting_page.dart b/lib/feature/presentation/page/setting/setting_page.dart index 926ad9c..ba815f3 100644 --- a/lib/feature/presentation/page/setting/setting_page.dart +++ b/lib/feature/presentation/page/setting/setting_page.dart @@ -35,7 +35,7 @@ class SettingPage extends StatefulWidget { static const routePath = '/setting'; static const routeName = 'setting'; - const SettingPage({Key? key}) : super(key: key); + const SettingPage({super.key}); @override State createState() => _SettingPageState(); diff --git a/lib/feature/presentation/page/setting_discord/setting_discord_page.dart b/lib/feature/presentation/page/setting_discord/setting_discord_page.dart index 7788c99..06a7060 100644 --- a/lib/feature/presentation/page/setting_discord/setting_discord_page.dart +++ b/lib/feature/presentation/page/setting_discord/setting_discord_page.dart @@ -16,7 +16,7 @@ class SettingDiscordPage extends StatefulWidget { static const routePath = '/setting-discord'; static const routeName = 'setting-discord'; - const SettingDiscordPage({Key? key}) : super(key: key); + const SettingDiscordPage({super.key}); @override State createState() => _SettingDiscordPageState(); diff --git a/lib/feature/presentation/page/setting_member_blur_screenshot/setting_member_blur_screenshot_page.dart b/lib/feature/presentation/page/setting_member_blur_screenshot/setting_member_blur_screenshot_page.dart index d9341e0..6db57be 100644 --- a/lib/feature/presentation/page/setting_member_blur_screenshot/setting_member_blur_screenshot_page.dart +++ b/lib/feature/presentation/page/setting_member_blur_screenshot/setting_member_blur_screenshot_page.dart @@ -16,7 +16,7 @@ class SettingMemberBlurScreenshotPage extends StatefulWidget { static const routePath = '/member-blur-screenshot'; static const routeName = 'member-blur-screenshot'; - const SettingMemberBlurScreenshotPage({Key? key}) : super(key: key); + const SettingMemberBlurScreenshotPage({super.key}); @override State createState() => _SettingMemberBlurScreenshotPageState(); diff --git a/lib/feature/presentation/page/setup_credential/setup_credential_page.dart b/lib/feature/presentation/page/setup_credential/setup_credential_page.dart index 110904b..4a7ec98 100644 --- a/lib/feature/presentation/page/setup_credential/setup_credential_page.dart +++ b/lib/feature/presentation/page/setup_credential/setup_credential_page.dart @@ -2,10 +2,14 @@ import 'package:dipantau_desktop_client/core/util/enum/global_variable.dart'; import 'package:dipantau_desktop_client/core/util/helper.dart'; import 'package:dipantau_desktop_client/core/util/shared_preferences_manager.dart'; import 'package:dipantau_desktop_client/core/util/widget_helper.dart'; +import 'package:dipantau_desktop_client/feature/presentation/bloc/setup_credential/setup_credential_bloc.dart'; +import 'package:dipantau_desktop_client/feature/presentation/widget/widget_loading_center_full_screen.dart'; import 'package:dipantau_desktop_client/feature/presentation/widget/widget_primary_button.dart'; import 'package:dipantau_desktop_client/injection_container.dart' as di; +import 'package:dipantau_desktop_client/injection_container.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; class SetupCredentialPage extends StatefulWidget { @@ -18,10 +22,10 @@ class SetupCredentialPage extends StatefulWidget { final bool isShowWarning; const SetupCredentialPage({ - Key? key, + super.key, this.isFromSplashScreen = false, this.isShowWarning = false, - }) : super(key: key); + }); @override State createState() => _SetupCredentialPageState(); @@ -32,6 +36,7 @@ class _SetupCredentialPageState extends State { final controllerHostname = TextEditingController(); final widgetHelper = WidgetHelper(); final formState = GlobalKey(); + final setupCredentialBloc = sl(); var isLogin = false; var defaultDomainApi = ''; @@ -52,37 +57,81 @@ class _SetupCredentialPageState extends State { appBar: AppBar( automaticallyImplyLeading: widget.isFromSplashScreen ? false : true, ), - body: Padding( - padding: EdgeInsets.only( - left: helper.getDefaultPaddingLayout, - top: helper.getDefaultPaddingLayoutTop, - right: helper.getDefaultPaddingLayout, - bottom: helper.getDefaultPaddingLayout, + body: BlocProvider( + create: (context) => setupCredentialBloc, + child: BlocListener( + listener: (context, state) { + if (state is FailureSetupCredentialState) { + final errorMessage = 'invalid_hostname'.tr(); + widgetHelper.showDialogMessage( + context, + 'info'.tr(), + errorMessage, + ); + } else if (state is SuccessPingSetupCredentialState) { + final hostname = state.baseUrl; + sharedPreferencesManager.putString(SharedPreferencesManager.keyDomainApi, hostname).then((value) { + helper.setDomainApiToFlavor(hostname); + di.init(); + if (mounted) { + context.go('/'); + } + }); + } + }, + child: Stack( + children: [ + buildWidgetBody(), + buildWidgetLoadingOverlay(), + ], + ), ), - child: SizedBox( - width: double.infinity, - child: Form( - key: formState, - autovalidateMode: AutovalidateMode.onUserInteraction, - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - buildWidgetTitle(), - const SizedBox(height: 8), - Text( - 'subtitle_set_hostname'.tr(), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.grey, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 24), - buildWidgetTextFieldHostname(), - const SizedBox(height: 24), - buildWidgetButtonSave(), - ], - ), + ), + ); + } + + Widget buildWidgetLoadingOverlay() { + return BlocBuilder( + builder: (context, state) { + if (state is LoadingSetupCredentialState) { + return const WidgetLoadingCenterFullScreen(); + } + return Container(); + }, + ); + } + + Widget buildWidgetBody() { + return Padding( + padding: EdgeInsets.only( + left: helper.getDefaultPaddingLayout, + top: helper.getDefaultPaddingLayoutTop, + right: helper.getDefaultPaddingLayout, + bottom: helper.getDefaultPaddingLayout, + ), + child: SizedBox( + width: double.infinity, + child: Form( + key: formState, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + buildWidgetTitle(), + const SizedBox(height: 8), + Text( + 'subtitle_set_hostname'.tr(), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.grey, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + buildWidgetTextFieldHostname(), + const SizedBox(height: 24), + buildWidgetButtonSave(), + ], ), ), ), @@ -148,13 +197,12 @@ class _SetupCredentialPageState extends State { ) as bool?; } if (isContinue != null && isContinue) { - final hostname = controllerHostname.text.trim(); - await sharedPreferencesManager.putString(SharedPreferencesManager.keyDomainApi, hostname); - helper.setDomainApiToFlavor(hostname); - di.init(); - if (mounted) { - context.go('/'); - } + final hostname = helper.removeTrailingSlash(controllerHostname.text.trim()).trim(); + setupCredentialBloc.add( + PingSetupCredentialEvent( + baseUrl: hostname, + ), + ); } } } diff --git a/lib/feature/presentation/page/splash/splash_page.dart b/lib/feature/presentation/page/splash/splash_page.dart index 40519f0..f097eb7 100644 --- a/lib/feature/presentation/page/splash/splash_page.dart +++ b/lib/feature/presentation/page/splash/splash_page.dart @@ -13,7 +13,7 @@ class SplashPage extends StatefulWidget { static const routePath = '/splash'; static const routeName = 'splash'; - const SplashPage({Key? key}) : super(key: key); + const SplashPage({super.key}); @override State createState() => _SplashPageState(); diff --git a/lib/feature/presentation/page/sync/sync_page.dart b/lib/feature/presentation/page/sync/sync_page.dart index 0f1bfa9..7654729 100644 --- a/lib/feature/presentation/page/sync/sync_page.dart +++ b/lib/feature/presentation/page/sync/sync_page.dart @@ -26,7 +26,7 @@ class SyncPage extends StatefulWidget { static const routePath = '/sync'; static const routeName = 'sync'; - const SyncPage({Key? key}) : super(key: key); + const SyncPage({super.key}); @override State createState() => _SyncPageState(); diff --git a/lib/feature/presentation/page/user_registration_setting/user_registration_setting_page.dart b/lib/feature/presentation/page/user_registration_setting/user_registration_setting_page.dart index 3a2a4a8..4fcb0cf 100644 --- a/lib/feature/presentation/page/user_registration_setting/user_registration_setting_page.dart +++ b/lib/feature/presentation/page/user_registration_setting/user_registration_setting_page.dart @@ -17,7 +17,7 @@ class UserRegistrationSettingPage extends StatefulWidget { static const routePath = '/user-registration-setting'; static const routeName = 'user-registration-setting'; - const UserRegistrationSettingPage({Key? key}) : super(key: key); + const UserRegistrationSettingPage({super.key}); @override State createState() => _UserRegistrationSettingPageState(); diff --git a/lib/feature/presentation/page/verify_forgot_password/verify_forgot_password_page.dart b/lib/feature/presentation/page/verify_forgot_password/verify_forgot_password_page.dart index efc62cc..4d27951 100644 --- a/lib/feature/presentation/page/verify_forgot_password/verify_forgot_password_page.dart +++ b/lib/feature/presentation/page/verify_forgot_password/verify_forgot_password_page.dart @@ -20,9 +20,9 @@ class VerifyForgotPasswordPage extends StatefulWidget { final String email; const VerifyForgotPasswordPage({ - Key? key, + super.key, required this.email, - }) : super(key: key); + }); @override State createState() => _VerifyForgotPasswordPageState(); diff --git a/lib/feature/presentation/widget/widget_choose_project.dart b/lib/feature/presentation/widget/widget_choose_project.dart index 7a8c1dc..fb34d83 100644 --- a/lib/feature/presentation/widget/widget_choose_project.dart +++ b/lib/feature/presentation/widget/widget_choose_project.dart @@ -14,8 +14,8 @@ class WidgetChooseProject extends StatefulWidget { const WidgetChooseProject({ required this.defaultSelectedProjectId, - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _WidgetChooseProjectState(); diff --git a/lib/feature/presentation/widget/widget_custom_circular_progress_indicator.dart b/lib/feature/presentation/widget/widget_custom_circular_progress_indicator.dart index 94dde8e..9391872 100644 --- a/lib/feature/presentation/widget/widget_custom_circular_progress_indicator.dart +++ b/lib/feature/presentation/widget/widget_custom_circular_progress_indicator.dart @@ -4,9 +4,9 @@ class WidgetCustomCircularProgressIndicator extends StatelessWidget { final Color? color; const WidgetCustomCircularProgressIndicator({ - Key? key, + super.key, this.color, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/feature/presentation/widget/widget_error.dart b/lib/feature/presentation/widget/widget_error.dart index 219ab01..194db11 100644 --- a/lib/feature/presentation/widget/widget_error.dart +++ b/lib/feature/presentation/widget/widget_error.dart @@ -9,11 +9,11 @@ class WidgetError extends StatelessWidget { final Function()? onTryAgain; const WidgetError({ - Key? key, + super.key, required this.title, required this.message, this.onTryAgain, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/feature/presentation/widget/widget_icon_circle.dart b/lib/feature/presentation/widget/widget_icon_circle.dart index f11585c..eee7421 100644 --- a/lib/feature/presentation/widget/widget_icon_circle.dart +++ b/lib/feature/presentation/widget/widget_icon_circle.dart @@ -9,8 +9,8 @@ class WidgetIconCircle extends StatelessWidget { required this.iconData, this.size = 32.0, this.padding = 16.0, - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/lib/feature/presentation/widget/widget_loading_center_full_screen.dart b/lib/feature/presentation/widget/widget_loading_center_full_screen.dart index 724f4a0..54c68fb 100644 --- a/lib/feature/presentation/widget/widget_loading_center_full_screen.dart +++ b/lib/feature/presentation/widget/widget_loading_center_full_screen.dart @@ -2,7 +2,7 @@ import 'package:dipantau_desktop_client/feature/presentation/widget/widget_custo import 'package:flutter/material.dart'; class WidgetLoadingCenterFullScreen extends StatelessWidget { - const WidgetLoadingCenterFullScreen({Key? key}) : super(key: key); + const WidgetLoadingCenterFullScreen({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/feature/presentation/widget/widget_primary_button.dart b/lib/feature/presentation/widget/widget_primary_button.dart index fe920d9..aeffeb5 100644 --- a/lib/feature/presentation/widget/widget_primary_button.dart +++ b/lib/feature/presentation/widget/widget_primary_button.dart @@ -8,12 +8,12 @@ class WidgetPrimaryButton extends StatelessWidget { final ButtonStyle? buttonStyle; const WidgetPrimaryButton({ - Key? key, + super.key, required this.onPressed, required this.child, this.isLoading, this.buttonStyle, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/feature/presentation/widget/widget_theme_container.dart b/lib/feature/presentation/widget/widget_theme_container.dart index cca0062..81e58c4 100644 --- a/lib/feature/presentation/widget/widget_theme_container.dart +++ b/lib/feature/presentation/widget/widget_theme_container.dart @@ -8,12 +8,12 @@ class WidgetThemeContainer extends StatefulWidget { final Color? borderColor; const WidgetThemeContainer({ - Key? key, + super.key, required this.mode, required this.width, required this.height, this.borderColor, - }) : super(key: key); + }); @override State createState() => _WidgetThemeContainerState(); diff --git a/lib/injection_container.dart b/lib/injection_container.dart index 9d581a2..a72e1a5 100644 --- a/lib/injection_container.dart +++ b/lib/injection_container.dart @@ -7,17 +7,20 @@ import 'package:dipantau_desktop_client/core/util/notification_helper.dart'; import 'package:dipantau_desktop_client/core/util/shared_preferences_manager.dart'; import 'package:dipantau_desktop_client/core/util/widget_helper.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/auth/auth_remote_data_source.dart'; +import 'package:dipantau_desktop_client/feature/data/datasource/general/general_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/project/project_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/setting/setting_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/track/track_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/user/user_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/repository/auth/auth_repository_impl.dart'; +import 'package:dipantau_desktop_client/feature/data/repository/general/general_repository_impl.dart'; import 'package:dipantau_desktop_client/feature/data/repository/project/project_repository_impl.dart'; import 'package:dipantau_desktop_client/feature/data/repository/setting/setting_repository_impl.dart'; import 'package:dipantau_desktop_client/feature/data/repository/track/track_repository_impl.dart'; import 'package:dipantau_desktop_client/feature/data/repository/user/user_repository_impl.dart'; import 'package:dipantau_desktop_client/feature/database/app_database.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/auth/auth_repository.dart'; +import 'package:dipantau_desktop_client/feature/domain/repository/general/general_repository.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/project/project_repository.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/setting/setting_repository.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/track/track_repository.dart'; @@ -38,6 +41,7 @@ import 'package:dipantau_desktop_client/feature/domain/usecase/get_track_user/ge import 'package:dipantau_desktop_client/feature/domain/usecase/get_track_user_lite/get_track_user_lite.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/get_user_setting/get_user_setting.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/login/login.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/ping/ping.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/refresh_token/refresh_token.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/reset_password/reset_password.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/send_app_version/send_app_version.dart'; @@ -57,6 +61,7 @@ import 'package:dipantau_desktop_client/feature/presentation/bloc/project/projec import 'package:dipantau_desktop_client/feature/presentation/bloc/report_screenshot/report_screenshot_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/reset_password/reset_password_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/setting/setting_bloc.dart'; +import 'package:dipantau_desktop_client/feature/presentation/bloc/setup_credential/setup_credential_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/sign_up/sign_up_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/sync_manual/sync_manual_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/tracking/tracking_bloc.dart'; @@ -169,6 +174,12 @@ void init() { getProjectTaskByUserId: sl(), ), ); + sl.registerFactory( + () => SetupCredentialBloc( + helper: sl(), + ping: sl(), + ), + ); // use case sl.registerLazySingleton(() => GetProject(repository: sl())); @@ -195,6 +206,7 @@ void init() { sl.registerLazySingleton(() => GetAllUserSetting(repository: sl())); sl.registerLazySingleton(() => GetUserSetting(repository: sl())); sl.registerLazySingleton(() => UpdateUserSetting(repository: sl())); + sl.registerLazySingleton(() => Ping(repository: sl())); // repository sl.registerLazySingleton( @@ -227,6 +239,12 @@ void init() { networkInfo: sl(), ), ); + sl.registerLazySingleton( + () => GeneralRepositoryImpl( + remoteDataSource: sl(), + networkInfo: sl(), + ), + ); // data source sl.registerLazySingleton( @@ -254,6 +272,11 @@ void init() { dio: sl(instanceName: dioRefreshToken), ), ); + sl.registerLazySingleton( + () => GeneralRemoteDataSourceImpl( + dio: sl(instanceName: dioLogging), + ), + ); // core sl.registerLazySingleton(() => NetworkInfoImpl(sl())); diff --git a/lib/main.dart b/lib/main.dart index 9d2def4..0d31dbe 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -168,14 +168,14 @@ class _MyAppState extends State { path: RegisterSuccessPage.routePath, name: RegisterSuccessPage.routeName, builder: (context, state) => RegisterSuccessPage( - email: state.queryParams['email'] as String, + email: state.uri.queryParameters['email'] as String, ), ), GoRoute( path: ForgotPasswordPage.routePath, name: ForgotPasswordPage.routeName, builder: (context, state) => ForgotPasswordPage( - email: state.queryParams['email'], + email: state.uri.queryParameters['email'], ), ), GoRoute( @@ -411,8 +411,14 @@ class _MyAppState extends State { isDarkMode = state.isDarkMode; } - return WillPopScope( - onWillPop: () async => false, + return PopScope( + canPop: false, + onPopInvoked: (didPop) { + if (didPop) { + return; + } + return; + }, child: MaterialApp.router( title: 'Dipantau', themeMode: isDarkMode ? ThemeMode.dark : ThemeMode.light, diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d0bc274..44a932a 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,7 +5,7 @@ import FlutterMacOS import Foundation -import auto_updater +import auto_updater_macos import connectivity_plus import flutter_local_notifications import package_info_plus @@ -17,10 +17,10 @@ import tray_manager import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - AutoUpdaterPlugin.register(with: registry.registrar(forPlugin: "AutoUpdaterPlugin")) - ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) + AutoUpdaterMacosPlugin.register(with: registry.registrar(forPlugin: "AutoUpdaterMacosPlugin")) + ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) - FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 484b54c..2aca182 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,60 +1,54 @@ PODS: - - auto_updater (0.0.1): + - auto_updater_macos (0.0.1): - FlutterMacOS - Sparkle - connectivity_plus (0.0.1): + - Flutter - FlutterMacOS - - ReachabilitySwift - flutter_local_notifications (0.0.1): - FlutterMacOS - FlutterMacOS (1.0.0) - - FMDB (2.7.5): - - FMDB/standard (= 2.7.5) - - FMDB/standard (2.7.5) - package_info_plus (0.0.1): - FlutterMacOS - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - ReachabilitySwift (5.0.0) - screen_retriever (0.0.1): - FlutterMacOS - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - Sparkle (2.4.2) - - sqflite (0.0.2): + - Sparkle (2.6.2) + - sqflite (0.0.3): + - Flutter - FlutterMacOS - - FMDB (>= 2.7.5) - tray_manager (0.0.1): - FlutterMacOS - window_manager (0.2.0): - FlutterMacOS DEPENDENCIES: - - auto_updater (from `Flutter/ephemeral/.symlinks/plugins/auto_updater/macos`) - - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) + - auto_updater_macos (from `Flutter/ephemeral/.symlinks/plugins/auto_updater_macos/macos`) + - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`) - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) + - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) - tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`) - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) SPEC REPOS: trunk: - - FMDB - - ReachabilitySwift - Sparkle EXTERNAL SOURCES: - auto_updater: - :path: Flutter/ephemeral/.symlinks/plugins/auto_updater/macos + auto_updater_macos: + :path: Flutter/ephemeral/.symlinks/plugins/auto_updater_macos/macos connectivity_plus: - :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos + :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin flutter_local_notifications: :path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos FlutterMacOS: @@ -68,25 +62,23 @@ EXTERNAL SOURCES: shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin sqflite: - :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos + :path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin tray_manager: :path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos window_manager: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - auto_updater: d3c03e9e5f2a00ec78572d9f7473cb8c9a6c0273 - connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 + auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d + connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a - package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 - ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 + package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 - Sparkle: 5ef7097e655c60f4aeb23fd1658fc3e8dd50f4ec - sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + Sparkle: a62c7dc4f410ced73beb2169cf1d3cc3f028a295 + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 364c46d..7d17195 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -208,7 +208,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 49ffbfe..47348a2 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ tHasConnection); + final tHasConnection = [ConnectivityResult.mobile]; + when(mockConnectivity.checkConnectivity()).thenAnswer((_) async => tHasConnection); // act final result = await networkInfoImpl.isConnected; @@ -36,8 +36,8 @@ void main() { 'menggunakan wifi', () async { // arrange - final tHasConnection = Future.value(ConnectivityResult.wifi); - when(mockConnectivity.checkConnectivity()).thenAnswer((_) => tHasConnection); + final tHasConnection = [ConnectivityResult.wifi]; + when(mockConnectivity.checkConnectivity()).thenAnswer((_) async => tHasConnection); // act final result = await networkInfoImpl.isConnected; @@ -53,8 +53,8 @@ void main() { 'tidak terhubung sama sekali', () async { // arrange - final tHasConnection = Future.value(ConnectivityResult.none); - when(mockConnectivity.checkConnectivity()).thenAnswer((_) => tHasConnection); + final tHasConnection = [ConnectivityResult.none]; + when(mockConnectivity.checkConnectivity()).thenAnswer((_) async => tHasConnection); // act final result = await networkInfoImpl.isConnected; diff --git a/test/feature/data/datasource/general/general_remote_data_source_test.dart b/test/feature/data/datasource/general/general_remote_data_source_test.dart new file mode 100644 index 0000000..ad0373b --- /dev/null +++ b/test/feature/data/datasource/general/general_remote_data_source_test.dart @@ -0,0 +1,109 @@ +import 'dart:convert'; + +import 'package:dio/dio.dart'; +import 'package:dipantau_desktop_client/config/flavor_config.dart'; +import 'package:dipantau_desktop_client/feature/data/datasource/general/general_remote_data_source.dart'; +import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import '../../../../fixture/fixture_reader.dart'; +import '../../../../helper/mock_helper.mocks.dart'; + +void main() { + late GeneralRemoteDataSource remoteDataSource; + late MockDio mockDio; + late MockHttpClientAdapter mockDioAdapter; + + const baseUrl = 'https://example.com'; + + setUp(() { + FlavorConfig( + values: FlavorValues( + baseUrl: baseUrl, + baseUrlAuth: '', + baseUrlUser: '', + baseUrlTrack: '', + baseUrlProject: '', + baseUrlSetting: '', + ), + ); + mockDio = MockDio(); + mockDioAdapter = MockHttpClientAdapter(); + mockDio.httpClientAdapter = mockDioAdapter; + remoteDataSource = GeneralRemoteDataSourceImpl(dio: mockDio); + }); + + final tRequestOptions = RequestOptions(path: ''); + + group('ping', () { + const tPathResponse = 'general_response.json'; + const hostname = baseUrl; + final tResponse = GeneralResponse.fromJson( + json.decode( + fixture(tPathResponse), + ), + ); + + void setUpMockDioSuccess() { + final responsePayload = json.decode(fixture(tPathResponse)); + final response = Response( + requestOptions: tRequestOptions, + data: responsePayload, + statusCode: 200, + headers: Headers.fromMap({ + Headers.contentTypeHeader: [Headers.jsonContentType], + }), + ); + when(mockDio.get(any)).thenAnswer((_) async => response); + } + + test( + 'pastikan endpoint ping benar-benar terpanggil dengan method GET', + () async { + // arrange + setUpMockDioSuccess(); + + // act + await remoteDataSource.ping(hostname); + + // assert + verify(mockDio.get('$hostname/api/ping')); + }, + ); + + test( + 'pastikan mengembalikan objek class model GeneralResponse ketika menerima respon sukses ' + 'dari endpoint', + () async { + // arrange + setUpMockDioSuccess(); + + // act + final result = await remoteDataSource.ping(hostname); + + // assert + expect(result, tResponse); + }, + ); + + test( + 'pastikan akan menerima exception DioError ketika menerima respon kegagalan dari endpoint', + () async { + // arrange + final response = Response( + requestOptions: tRequestOptions, + data: 'Bad Request', + statusCode: 400, + ); + when(mockDio.get(any)).thenAnswer((_) async => response); + + // act + final call = remoteDataSource.ping(hostname); + + // assert + expect(() => call, throwsA(const TypeMatcher())); + }, + ); + }); +} diff --git a/test/feature/data/repository/general/general_repository_impl_test.dart b/test/feature/data/repository/general/general_repository_impl_test.dart new file mode 100644 index 0000000..82c1d33 --- /dev/null +++ b/test/feature/data/repository/general/general_repository_impl_test.dart @@ -0,0 +1,189 @@ +import 'dart:convert'; + +import 'package:dio/dio.dart'; +import 'package:dipantau_desktop_client/core/error/failure.dart'; +import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; +import 'package:dipantau_desktop_client/feature/data/repository/general/general_repository_impl.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import '../../../../fixture/fixture_reader.dart'; +import '../../../../helper/mock_helper.mocks.dart'; + +void main() { + late GeneralRepositoryImpl repository; + late MockGeneralRemoteDataSource mockRemoteDataSource; + late MockNetworkInfo mockNetworkInfo; + + setUp(() { + mockRemoteDataSource = MockGeneralRemoteDataSource(); + mockNetworkInfo = MockNetworkInfo(); + repository = GeneralRepositoryImpl( + remoteDataSource: mockRemoteDataSource, + networkInfo: mockNetworkInfo, + ); + }); + + final tRequestOptions = RequestOptions(path: ''); + + void setUpMockNetworkConnected() { + when(mockNetworkInfo.isConnected).thenAnswer((_) async => true); + } + + void setUpMockNetworkDisconnected() { + when(mockNetworkInfo.isConnected).thenAnswer((_) async => false); + } + + void testDisconnected(Function endpointInvoke) { + test( + 'pastikan mengembalikan objek ConnectionFailure ketika device tidak terhubung ke internet', + () async { + // arrange + setUpMockNetworkDisconnected(); + + // act + final result = await endpointInvoke.call(); + + // assert + verify(mockNetworkInfo.isConnected); + expect(result.failure, ConnectionFailure()); + }, + ); + } + + void testServerFailureString(Function whenInvoke, Function actInvoke, Function verifyInvoke) { + test( + 'pastikan mengembalikan objek ServerFailure ketika repository menerima respon kegagalan ' + 'dari endpoint dengan respon data html atau string', + () async { + // arrange + setUpMockNetworkConnected(); + when(whenInvoke.call()).thenThrow( + DioException( + requestOptions: tRequestOptions, + message: 'testError', + response: Response( + requestOptions: tRequestOptions, + data: 'testDataError', + statusCode: 400, + ), + ), + ); + + // act + final result = await actInvoke.call(); + + // assert + verify(verifyInvoke.call()); + expect(result.failure, ServerFailure('testError')); + }, + ); + } + + void testParsingFailure(Function whenInvoke, Function actInvoke, Function verifyInvoke) { + test( + 'pastikan mengembalikan objek ParsingFailure ketika RemoteDataSource menerima respon kegagalan ' + 'dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(whenInvoke.call()).thenThrow(TypeError()); + + // act + final result = await actInvoke.call(); + + // assert + verify(verifyInvoke.call()); + expect(result.failure, ParsingFailure(TypeError().toString())); + }, + ); + } + + group('ping', () { + final tResponse = GeneralResponse.fromJson( + json.decode( + fixture('general_response.json'), + ), + ); + const hostname = 'https://example.com'; + + test( + 'pastikan mengembalikan objek model GeneralResponse ketika RemoteDataSource berhasil menerima ' + 'respon sukses dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(mockRemoteDataSource.ping(any)).thenAnswer((_) async => tResponse); + + // act + final result = await repository.ping(hostname); + + // assert + verify(mockRemoteDataSource.ping(hostname)); + expect(result.response, tResponse); + }, + ); + + test( + 'pastikan mengembalikan objek ServerFailure ketika RemoteDataSource berhasil menerima ' + 'respon timeout dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(mockRemoteDataSource.ping(any)) + .thenThrow(DioException(requestOptions: tRequestOptions, message: 'testError')); + + // act + final result = await repository.ping(hostname); + + // assert + verify(mockRemoteDataSource.ping(hostname)); + expect(result.failure, ServerFailure('testError')); + }, + ); + + test( + 'pastikan mengembalikan objek ServerFailure ketika RemoteDataSource menerima respon kegagaln ' + 'dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(mockRemoteDataSource.ping(any)).thenThrow( + DioException( + requestOptions: tRequestOptions, + message: 'testError', + response: Response( + requestOptions: tRequestOptions, + data: { + 'title': 'testTitleError', + 'message': 'testMessageError', + }, + statusCode: 400, + ), + ), + ); + + // act + final result = await repository.ping(hostname); + + // assert + verify(mockRemoteDataSource.ping(hostname)); + expect(result.failure, ServerFailure('400 testMessageError')); + }, + ); + + testServerFailureString( + () => mockRemoteDataSource.ping(any), + () => repository.ping(hostname), + () => mockRemoteDataSource.ping(hostname), + ); + + testParsingFailure( + () => mockRemoteDataSource.ping(any), + () => repository.ping(hostname), + () => mockRemoteDataSource.ping(hostname), + ); + + testDisconnected(() => repository.ping(hostname)); + }); +} diff --git a/test/feature/domain/usecase/ping/ping_test.dart b/test/feature/domain/usecase/ping/ping_test.dart new file mode 100644 index 0000000..609071e --- /dev/null +++ b/test/feature/domain/usecase/ping/ping_test.dart @@ -0,0 +1,68 @@ +import 'dart:convert'; + +import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/ping/ping.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import '../../../../fixture/fixture_reader.dart'; +import '../../../../helper/mock_helper.mocks.dart'; + +void main() { + late Ping useCase; + late MockGeneralRepository mockRepository; + + setUp(() { + mockRepository = MockGeneralRepository(); + useCase = Ping(repository: mockRepository); + }); + + const hostname = 'https://example.com'; + final tParams = ParamsPing(baseUrl: hostname); + + test( + 'pastikan objek repository berhasil menerima respon sukses atau gagal dari endpoint', + () async { + // arrange + final tResponse = GeneralResponse.fromJson( + json.decode( + fixture('general_response.json'), + ), + ); + final tResult = (failure: null, response: tResponse); + when(mockRepository.ping(any)).thenAnswer((_) async => tResult); + + // act + final result = await useCase(tParams); + + // assert + expect(result, tResult); + verify(mockRepository.ping(hostname)); + verifyNoMoreInteractions(mockRepository); + }, + ); + + test( + 'pastikan output dari nilai props', + () async { + // assert + expect( + tParams.props, + [ + tParams.baseUrl, + ], + ); + }, + ); + + test( + 'pastikan output dari fungsi toString', + () async { + // assert + expect( + tParams.toString(), + 'ParamsPing{baseUrl: ${tParams.baseUrl}}', + ); + }, + ); +} diff --git a/test/feature/presentation/bloc/setup_credential/setup_credential_bloc_test.dart b/test/feature/presentation/bloc/setup_credential/setup_credential_bloc_test.dart new file mode 100644 index 0000000..7da245a --- /dev/null +++ b/test/feature/presentation/bloc/setup_credential/setup_credential_bloc_test.dart @@ -0,0 +1,131 @@ +import 'dart:convert'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:dipantau_desktop_client/core/error/failure.dart'; +import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/ping/ping.dart'; +import 'package:dipantau_desktop_client/feature/presentation/bloc/setup_credential/setup_credential_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import '../../../../fixture/fixture_reader.dart'; +import '../../../../helper/mock_helper.mocks.dart'; + +void main() { + late SetupCredentialBloc bloc; + late MockHelper mockHelper; + late MockPing mockPing; + + const errorMessage = 'testErrorMessage'; + + setUp(() { + mockHelper = MockHelper(); + mockPing = MockPing(); + bloc = SetupCredentialBloc( + helper: mockHelper, + ping: mockPing, + ); + }); + + test( + 'pastikan output dari initial state', + () async { + // assert + expect( + bloc.state, + isA(), + ); + }, + ); + + group('ping setup credential', () { + const baseUrl = 'https://example.com'; + final params = ParamsPing(baseUrl: baseUrl); + final event = PingSetupCredentialEvent(baseUrl: baseUrl); + + blocTest( + 'pastikan emit [LoadingSetupCredentialState, SuccessPingSetupCredentialState] ketika terima event ' + 'PingSetupCredentialEvent dengan proses berhasil', + build: () { + final response = GeneralResponse.fromJson( + json.decode( + fixture('general_response.json'), + ), + ); + final result = (failure: null, response: response); + when(mockPing(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (SetupCredentialBloc bloc) { + return bloc.add(event); + }, + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockPing(params)); + }, + ); + + blocTest( + 'pastikan emit [LoadingSetupCredentialState, FailureSetupCredentialState] ketika terima event ' + 'PingSetupCredentialEvent dengan proses gagal dari endpoint', + build: () { + final result = (failure: ServerFailure(errorMessage), response: null); + when(mockPing(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (SetupCredentialBloc bloc) { + return bloc.add(event); + }, + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockPing(params)); + }, + ); + + blocTest( + 'pastikan emit [LoadingSetupCredentialState, FailureSetupCredentialState] ketika terima event ' + 'PingSetupCredentialEvent dengan kondisi internet tidak terhubung ketika hit endpoint', + build: () { + final result = (failure: ConnectionFailure(), response: null); + when(mockPing(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (SetupCredentialBloc bloc) { + return bloc.add(event); + }, + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockPing(params)); + }, + ); + + blocTest( + 'pastikan emit [LoadingSetupCredentialState, FailureSetupCredentialState] ketika terima event ' + 'PingSetupCredentialEvent dengan proses gagal parsing respon JSON endpoint', + build: () { + final result = (failure: ParsingFailure(errorMessage), response: null); + when(mockPing(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (SetupCredentialBloc bloc) { + return bloc.add(event); + }, + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockPing(params)); + }, + ); + }); +} diff --git a/test/feature/presentation/bloc/setup_credential/setup_credential_event_test.dart b/test/feature/presentation/bloc/setup_credential/setup_credential_event_test.dart new file mode 100644 index 0000000..576022b --- /dev/null +++ b/test/feature/presentation/bloc/setup_credential/setup_credential_event_test.dart @@ -0,0 +1,19 @@ +import 'package:dipantau_desktop_client/feature/presentation/bloc/setup_credential/setup_credential_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('PingSetupCredentialEvent', () { + final event = PingSetupCredentialEvent(baseUrl: 'https://example.com'); + + test( + 'pastikan output dari fungsi toString', + () async { + // assert + expect( + event.toString(), + 'PingSetupCredentialEvent{baseUrl: ${event.baseUrl}}', + ); + }, + ); + }); +} diff --git a/test/feature/presentation/bloc/setup_credential/setup_credential_state_test.dart b/test/feature/presentation/bloc/setup_credential/setup_credential_state_test.dart new file mode 100644 index 0000000..f20f2ef --- /dev/null +++ b/test/feature/presentation/bloc/setup_credential/setup_credential_state_test.dart @@ -0,0 +1,34 @@ +import 'package:dipantau_desktop_client/feature/presentation/bloc/setup_credential/setup_credential_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('FailureSetupCredentialState', () { + final state = FailureSetupCredentialState(errorMessage: 'errorMessage'); + + test( + 'pastikan output dari fungsi toString', + () async { + // assert + expect( + state.toString(), + 'FailureSetupCredentialState{errorMessage: ${state.errorMessage}}', + ); + }, + ); + }); + + group('SuccessPingSetupCredentialState', () { + final state = SuccessPingSetupCredentialState(baseUrl: 'https://example.com'); + + test( + 'pastikan output dari fungsi toString', + () async { + // assert + expect( + state.toString(), + 'SuccessPingSetupCredentialState{baseUrl: ${state.baseUrl}}', + ); + }, + ); + }); +} diff --git a/test/helper/mock_helper.dart b/test/helper/mock_helper.dart index ab3f6c1..1913f43 100644 --- a/test/helper/mock_helper.dart +++ b/test/helper/mock_helper.dart @@ -4,11 +4,13 @@ import 'package:dipantau_desktop_client/core/network/network_info.dart'; import 'package:dipantau_desktop_client/core/util/helper.dart'; import 'package:dipantau_desktop_client/core/util/shared_preferences_manager.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/auth/auth_remote_data_source.dart'; +import 'package:dipantau_desktop_client/feature/data/datasource/general/general_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/project/project_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/setting/setting_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/track/track_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/user/user_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/auth/auth_repository.dart'; +import 'package:dipantau_desktop_client/feature/domain/repository/general/general_repository.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/project/project_repository.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/setting/setting_repository.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/track/track_repository.dart'; @@ -29,6 +31,7 @@ import 'package:dipantau_desktop_client/feature/domain/usecase/get_track_user/ge import 'package:dipantau_desktop_client/feature/domain/usecase/get_track_user_lite/get_track_user_lite.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/get_user_setting/get_user_setting.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/login/login.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/ping/ping.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/refresh_token/refresh_token.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/reset_password/reset_password.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/send_app_version/send_app_version.dart'; @@ -53,11 +56,13 @@ import 'package:shared_preferences/shared_preferences.dart'; MockSpec(), MockSpec(), MockSpec(), + MockSpec(), MockSpec(), MockSpec(), MockSpec(), MockSpec(), MockSpec(), + MockSpec(), MockSpec(), MockSpec(), MockSpec(), @@ -82,5 +87,6 @@ import 'package:shared_preferences/shared_preferences.dart'; MockSpec(), MockSpec(), MockSpec(), + MockSpec(), ]) void main() {} diff --git a/test/util/helper_test.dart b/test/util/helper_test.dart index ec5779d..06f184c 100644 --- a/test/util/helper_test.dart +++ b/test/util/helper_test.dart @@ -170,4 +170,25 @@ void main() { expect(unknownFailure, constantErrorMessage.failureUnknown); }, ); + + test( + 'pastikan function removeTrailingSlash bisa menghapus karakter "/" diakhir dari sebuah string.', + () async { + // arrange + const input = 'https://example.com/'; + const input2 = 'https://example.com'; + const input3 = 'https://example.com////'; + const output = 'https://example.com'; + + // act + final actual1 = helper.removeTrailingSlash(input); + final actual2 = helper.removeTrailingSlash(input2); + final actual3 = helper.removeTrailingSlash(input3); + + // assert + expect(actual1, output); + expect(actual2, output); + expect(actual3, output); + }, + ); } diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 6201f9f..70160cb 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,15 +6,15 @@ #include "generated_plugin_registrant.h" -#include +#include #include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { - AutoUpdaterPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("AutoUpdaterPlugin")); + AutoUpdaterWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AutoUpdaterWindowsPluginCApi")); ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); ScreenRetrieverPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index eb2f99c..2d258cd 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,7 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST - auto_updater + auto_updater_windows connectivity_plus screen_retriever tray_manager