diff --git a/lib/pages/auth/model/user_model.dart b/lib/pages/auth/model/user_model.dart index 84d4661f..5090b0e0 100644 --- a/lib/pages/auth/model/user_model.dart +++ b/lib/pages/auth/model/user_model.dart @@ -10,6 +10,10 @@ class UserModel { final String? phoneNumber; final bool? isEmailVerified; final bool? isAgreementAccepted; + final bool? hasAcceptedWebAgreement; + final DateTime? webAgreementAcceptedAt; + final UserRole? role; + UserModel({ required this.uuid, required this.email, @@ -19,6 +23,9 @@ class UserModel { required this.phoneNumber, required this.isEmailVerified, required this.isAgreementAccepted, + required this.hasAcceptedWebAgreement, + required this.webAgreementAcceptedAt, + required this.role, }); factory UserModel.fromJson(Map json) { @@ -31,6 +38,11 @@ class UserModel { phoneNumber: json['phoneNumber'], isEmailVerified: json['isEmailVerified'], isAgreementAccepted: json['isAgreementAccepted'], + hasAcceptedWebAgreement: json['hasAcceptedWebAgreement'], + webAgreementAcceptedAt: json['webAgreementAcceptedAt'] != null + ? DateTime.parse(json['webAgreementAcceptedAt']) + : null, + role: json['role'] != null ? UserRole.fromJson(json['role']) : null, ); } @@ -41,6 +53,9 @@ class UserModel { Map tempJson = Token.decodeToken(token.accessToken); return UserModel( + hasAcceptedWebAgreement: null, + role: null, + webAgreementAcceptedAt: null, uuid: tempJson['uuid'].toString(), email: tempJson['email'], firstName: null, @@ -65,3 +80,26 @@ class UserModel { }; } } + +class UserRole { + final String uuid; + final DateTime createdAt; + final DateTime updatedAt; + final String type; + + UserRole({ + required this.uuid, + required this.createdAt, + required this.updatedAt, + required this.type, + }); + + factory UserRole.fromJson(Map json) { + return UserRole( + uuid: json['uuid'], + createdAt: DateTime.parse(json['createdAt']), + updatedAt: DateTime.parse(json['updatedAt']), + type: json['type'], + ); + } +} diff --git a/lib/pages/home/bloc/home_bloc.dart b/lib/pages/home/bloc/home_bloc.dart index 416e9d92..8c887810 100644 --- a/lib/pages/home/bloc/home_bloc.dart +++ b/lib/pages/home/bloc/home_bloc.dart @@ -18,10 +18,15 @@ class HomeBloc extends Bloc { List sourcesList = []; List destinationsList = []; UserModel? user; + String terms = ''; + String policy = ''; HomeBloc() : super((HomeInitial())) { on(_createNode); on(_fetchUserInfo); + on(_fetchTerms); + on(_fetchPolicy); + on(_confirmUserAgreement); } void _createNode(CreateNewNode event, Emitter emit) async { @@ -45,12 +50,47 @@ class HomeBloc extends Bloc { var uuid = await const FlutterSecureStorage().read(key: UserModel.userUuidKey); user = await HomeApi().fetchUserInfo(uuid); + add(FetchTermEvent()); + add(FetchPolicyEvent()); emit(HomeInitial()); } catch (e) { return; } } + Future _fetchTerms(FetchTermEvent event, Emitter emit) async { + try { + emit(LoadingHome()); + terms = await HomeApi().fetchTerms(); + emit(TermsAgreement()); + } catch (e) { + return; + } + } + + Future _fetchPolicy(FetchPolicyEvent event, Emitter emit) async { + try { + emit(LoadingHome()); + policy = await HomeApi().fetchPolicy(); + emit(PolicyAgreement()); + } catch (e) { + return; + } + } + + Future _confirmUserAgreement( + ConfirmUserAgreementEvent event, Emitter emit) async { + try { + emit(LoadingHome()); + var uuid = + await const FlutterSecureStorage().read(key: UserModel.userUuidKey); + policy = await HomeApi().confirmUserAgreements(uuid); + emit(PolicyAgreement()); + } catch (e) { + return; + } + } + // static Future fetchUserInfo() async { // try { // var uuid = diff --git a/lib/pages/home/bloc/home_event.dart b/lib/pages/home/bloc/home_event.dart index 963202b9..91b3bee8 100644 --- a/lib/pages/home/bloc/home_event.dart +++ b/lib/pages/home/bloc/home_event.dart @@ -20,4 +20,8 @@ class CreateNewNode extends HomeEvent { class FetchUserInfo extends HomeEvent { const FetchUserInfo(); -} \ No newline at end of file +}class FetchTermEvent extends HomeEvent {} + +class FetchPolicyEvent extends HomeEvent {} + +class ConfirmUserAgreementEvent extends HomeEvent {} \ No newline at end of file diff --git a/lib/pages/home/bloc/home_state.dart b/lib/pages/home/bloc/home_state.dart index 10c50486..5640d550 100644 --- a/lib/pages/home/bloc/home_state.dart +++ b/lib/pages/home/bloc/home_state.dart @@ -7,8 +7,12 @@ abstract class HomeState extends Equatable { @override List get props => []; } +class LoadingHome extends HomeState {} class HomeInitial extends HomeState {} +class TermsAgreement extends HomeState {} + +class PolicyAgreement extends HomeState {} class HomeCounterState extends HomeState { final int counter; @@ -24,3 +28,5 @@ class HomeUpdateTree extends HomeState { @override List get props => [graph, builder]; } + +//FetchTermEvent \ No newline at end of file diff --git a/lib/pages/home/view/agreement_and_privacy_dialog.dart b/lib/pages/home/view/agreement_and_privacy_dialog.dart new file mode 100644 index 00000000..e9371ae9 --- /dev/null +++ b/lib/pages/home/view/agreement_and_privacy_dialog.dart @@ -0,0 +1,176 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:go_router/go_router.dart'; +import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/routes_const.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class AgreementAndPrivacyDialog extends StatefulWidget { + final String terms; + final String policy; + + const AgreementAndPrivacyDialog({ + super.key, + required this.terms, + required this.policy, + }); + + @override + _AgreementAndPrivacyDialogState createState() => + _AgreementAndPrivacyDialogState(); +} + +class _AgreementAndPrivacyDialogState extends State { + final ScrollController _scrollController = ScrollController(); + bool _isAtEnd = false; + int _currentPage = 1; + + @override + void initState() { + super.initState(); + _scrollController.addListener(_onScroll); + WidgetsBinding.instance + .addPostFrameCallback((_) => _checkScrollRequirement()); + } + + void _checkScrollRequirement() { + final scrollPosition = _scrollController.position; + if (scrollPosition.maxScrollExtent <= 0) { + setState(() { + _isAtEnd = true; + }); + } + } + + @override + void dispose() { + _scrollController.removeListener(_onScroll); + _scrollController.dispose(); + super.dispose(); + } + + void _onScroll() { + if (_scrollController.position.atEdge) { + final isAtBottom = _scrollController.position.pixels == + _scrollController.position.maxScrollExtent; + if (isAtBottom && !_isAtEnd) { + setState(() { + _isAtEnd = true; + }); + } + } + } + + String get _dialogTitle => + _currentPage == 2 ? 'User Agreement' : 'Privacy Policy'; + + String get _dialogContent => _currentPage == 2 ? widget.terms : widget.policy; + + Widget _buildScrollableContent() { + return Container( + padding: const EdgeInsets.all(40), + width: MediaQuery.of(context).size.width * 0.8, + height: MediaQuery.of(context).size.height * 0.75, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: const BorderRadius.all(Radius.circular(20)), + ), + child: Scrollbar( + thumbVisibility: true, + trackVisibility: true, + interactive: true, + controller: _scrollController, + child: SingleChildScrollView( + controller: _scrollController, + padding: const EdgeInsets.all(25), + child: Html( + data: _dialogContent, + onLinkTap: (url, attributes, element) async { + if (url != null) { + final uri = Uri.parse(url); + await launchUrl(uri, mode: LaunchMode.externalApplication); + } + }, + style: { + "body": Style( + fontSize: FontSize(14), + color: Colors.black87, + lineHeight: LineHeight(1.5), + ), + }, + ), + ), + ), + ); + } + + Widget _buildActionButton() { + final String buttonText = _currentPage == 2 ? "I Agree" : "Next"; + + return InkWell( + onTap: _isAtEnd + ? () { + if (_currentPage == 1) { + setState(() { + _currentPage = 2; + _isAtEnd = false; + _scrollController.jumpTo(0); + WidgetsBinding.instance + .addPostFrameCallback((_) => _checkScrollRequirement()); + }); + } else { + Navigator.of(context).pop(true); + } + } + : null, + child: Text( + buttonText, + style: TextStyle( + color: _isAtEnd ? ColorsManager.secondaryColor : Colors.grey, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Dialog( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + _dialogTitle, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: ColorsManager.secondaryColor, + ), + ), + ), + const Divider(), + _buildScrollableContent(), + const Divider(), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + InkWell( + onTap: () { + AuthBloc.logout(); + context.go(RoutesConst.auth); + }, + child: const Text("Cancel"), + ), + _buildActionButton(), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/home/view/home_page_web.dart b/lib/pages/home/view/home_page_web.dart index a198fa76..8e7225d2 100644 --- a/lib/pages/home/view/home_page_web.dart +++ b/lib/pages/home/view/home_page_web.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/home/bloc/home_event.dart'; +import 'package:syncrow_web/pages/home/view/agreement_and_privacy_dialog.dart'; import 'package:syncrow_web/pages/home/bloc/home_bloc.dart'; import 'package:syncrow_web/pages/home/bloc/home_state.dart'; import 'package:syncrow_web/pages/home/view/home_card.dart'; @@ -9,16 +11,40 @@ import 'package:syncrow_web/web_layout/web_scaffold.dart'; class HomeWebPage extends StatelessWidget { const HomeWebPage({super.key}); + @override Widget build(BuildContext context) { Size size = MediaQuery.of(context).size; + final homeBloc = BlocProvider.of(context); + return PopScope( canPop: false, onPopInvoked: (didPop) => false, child: BlocConsumer( - listener: (BuildContext context, state) {}, + listener: (BuildContext context, state) { + if (state is HomeInitial) { + if (homeBloc.user!.hasAcceptedWebAgreement == false) { + Future.delayed(const Duration(seconds: 1), () { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AgreementAndPrivacyDialog( + terms: homeBloc.terms, + policy: homeBloc.policy, + ); + }, + ).then((v) { + if (v != null) { + homeBloc.add(ConfirmUserAgreementEvent()); + homeBloc.add(const FetchUserInfo()); + } + }); + }); + } + } + }, builder: (context, state) { - final homeBloc = BlocProvider.of(context); return WebScaffold( enableMenuSidebar: false, appBarTitle: Row( @@ -52,7 +78,8 @@ class HomeWebPage extends StatelessWidget { width: size.width * 0.68, child: GridView.builder( itemCount: 3, //8 - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, crossAxisSpacing: 20.0, mainAxisSpacing: 20.0, @@ -64,7 +91,8 @@ class HomeWebPage extends StatelessWidget { active: homeBloc.homeItems[index].active!, name: homeBloc.homeItems[index].title!, img: homeBloc.homeItems[index].icon!, - onTap: () => homeBloc.homeItems[index].onPress(context), + onTap: () => + homeBloc.homeItems[index].onPress(context), ); }, ), diff --git a/lib/services/home_api.dart b/lib/services/home_api.dart index dfbaf4bf..c1e67add 100644 --- a/lib/services/home_api.dart +++ b/lib/services/home_api.dart @@ -12,4 +12,33 @@ class HomeApi { }); return response; } + + Future fetchTerms() async { + final response = await HTTPService().get( + path: ApiEndpoints.terms, + showServerMessage: true, + expectedResponseModel: (json) { + return json['data']; + }); + return response; + } + + Future fetchPolicy() async { + final response = await HTTPService().get( + path: ApiEndpoints.policy, + showServerMessage: true, + expectedResponseModel: (json) { + return json['data']; + }); + return response; + } + + Future confirmUserAgreements(uuid) async { + final response = await HTTPService().patch( + path: ApiEndpoints.userAgreements.replaceAll('{userUuid}', uuid!), + expectedResponseModel: (json) { + return json['data']; + }); + return response; + } } diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index d0331dac..454ec4e7 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -108,6 +108,7 @@ abstract class ApiEndpoints { static const String deleteUser = '/invite-user/{inviteUserUuid}'; static const String changeUserStatus = '/invite-user/{invitedUserUuid}/disable'; - - // static const String updateAutomation = '/automation/{automationId}'; + static const String terms = '/terms'; + static const String policy = '/policy'; + static const String userAgreements = '/user/agreements/web/{userUuid}'; } diff --git a/pubspec.yaml b/pubspec.yaml index 786a39c9..f4108d5c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,6 +54,8 @@ dependencies: time_picker_spinner: ^1.0.0 intl_phone_field: ^3.2.0 number_pagination: ^1.1.6 + url_launcher: ^6.2.5 + flutter_html: ^3.0.0-beta.2 dev_dependencies: flutter_test: