diff --git a/api/lib/cecr_unwomen/consumer.ex b/api/lib/cecr_unwomen/consumer.ex index b24bb67..475382c 100644 --- a/api/lib/cecr_unwomen/consumer.ex +++ b/api/lib/cecr_unwomen/consumer.ex @@ -122,9 +122,9 @@ defmodule CecrUnwomen.Consumer do {:ok, obj} -> case obj["action"] do "broadcast_remind_to_input" -> spawn(fn -> ScheduleWorker.schedule_to_send_noti_vi([obj["data"]]) end) _ -> raise("not detect action #{obj}") - - Basic.ack channel, tag end + Basic.ack channel, tag + {:error, _} -> Basic.reject channel, tag, requeue: false end diff --git a/api/lib/cecr_unwomen/fcm/fcm_payload.ex b/api/lib/cecr_unwomen/fcm/fcm_payload.ex index f2225e4..fe57b54 100644 --- a/api/lib/cecr_unwomen/fcm/fcm_payload.ex +++ b/api/lib/cecr_unwomen/fcm/fcm_payload.ex @@ -1,4 +1,14 @@ defmodule CecrUnwomen.Fcm.FcmPayload do + def create_payload_with_data(:both, token, notification, data) do + %{ + "message" => %{ + "token" => token, + "notification" => notification, + "data" => data + }, + } + end + def create_payload(:both, token, notification) do %{ "message" => %{ diff --git a/api/lib/cecr_unwomen/utils/helper.ex b/api/lib/cecr_unwomen/utils/helper.ex index 90fda4a..8961371 100644 --- a/api/lib/cecr_unwomen/utils/helper.ex +++ b/api/lib/cecr_unwomen/utils/helper.ex @@ -123,4 +123,25 @@ defmodule CecrUnwomen.Utils.Helper do DateTime.now!(timezone) |> Calendar.strftime("%d/%m/%Y") end + + def fold_fcm_token(map, extra_fields \\ %{}) do + map + |> Enum.reduce(%{}, fn item, acc -> + user_id = item["user_id"] + token = item["token"] + + base_map = Map.merge( + %{ + "user_id" => user_id, + "tokens" => [token] + }, + extra_fields + ) + + Map.update(acc, user_id, base_map, fn existing -> + Map.update(existing, "tokens", [token], fn tokens -> tokens ++ [token] end) + end) + end) + |> Map.values() + end end diff --git a/api/lib/cecr_unwomen/workers/fcm_worker.ex b/api/lib/cecr_unwomen/workers/fcm_worker.ex index 7dff394..a424987 100644 --- a/api/lib/cecr_unwomen/workers/fcm_worker.ex +++ b/api/lib/cecr_unwomen/workers/fcm_worker.ex @@ -6,7 +6,7 @@ defmodule CecrUnwomen.Workers.FcmWorker do @url_firebase_messaging "https://fcm.googleapis.com/v1/projects/cecr-unwomen/messages:send" - def send_firebase_notification(firebase_tokens, notification_field) do + def send_firebase_notification(firebase_tokens, notification_field, data \\ %{}) do server_firebase_token = GenServer.call(FcmStore, :get_token) headers = [{"Authorization", "Bearer #{server_firebase_token}"}] @@ -16,7 +16,13 @@ defmodule CecrUnwomen.Workers.FcmWorker do Enum.each(firebase_tokens, fn t -> token = t["token"] - payload = FcmPayload.create_payload(:both, token, notification_field) + # payload = FcmPayload.create_payload(:both, token, notification_field) + payload = cond do + data == %{} -> FcmPayload.create_payload(:both, token, notification_field) + true -> + data_valid = data |> Map.new(fn {k, v} -> {k, to_string(v)} end) + FcmPayload.create_payload_with_data(:both, token, notification_field, data_valid) + end # payload = cond do # t["platform"] == "android" -> FcmPayload.create_payload(:android, token, data_android_string) # t["platform"] == "ios" && apns_custom_field != nil -> FcmPayload.create_payload(:ios_custom, token, data_ios_string, apns_custom_field) diff --git a/api/lib/cecr_unwomen/workers/schedule_worker.ex b/api/lib/cecr_unwomen/workers/schedule_worker.ex index 87629d4..8351aa4 100644 --- a/api/lib/cecr_unwomen/workers/schedule_worker.ex +++ b/api/lib/cecr_unwomen/workers/schedule_worker.ex @@ -44,6 +44,10 @@ defmodule CecrUnwomen.Workers.ScheduleWorker do %{ "title" => "Nhập dữ liệu ngày #{local_time_string}", "body" => "Bạn chưa nhập dữ liệu cho hôm nay. Ấn vào thông báo để tiến hành nhập liệu." + }, + %{ + "type" => "remind_to_contribute", + "role_id" => role_id } ) end diff --git a/api/lib/cecr_unwomen_web/controllers/contribution_controller.ex b/api/lib/cecr_unwomen_web/controllers/contribution_controller.ex index 5d52470..b2f3efd 100644 --- a/api/lib/cecr_unwomen_web/controllers/contribution_controller.ex +++ b/api/lib/cecr_unwomen_web/controllers/contribution_controller.ex @@ -4,6 +4,7 @@ defmodule CecrUnwomenWeb.ContributionController do alias CecrUnwomenWeb.Models.{ User, + FirebaseToken, ScraperContribution, HouseholdContribution, OverallScraperContribution, @@ -11,7 +12,8 @@ defmodule CecrUnwomenWeb.ContributionController do HouseholdConstantFactor, ScrapConstantFactor } - + + alias CecrUnwomen.Workers.FcmWorker alias CecrUnwomen.{Utils.Helper, Repo} def contribute_data(conn, params) do @@ -58,7 +60,10 @@ defmodule CecrUnwomenWeb.ContributionController do Helper.aggregate_with_fields(OverallScraperContribution, keys) end) |> case do - {:ok, overall_data} -> Helper.response_json_with_data(true, "Nhập thông tin thành công!", overall_data) + {:ok, overall_data} -> + send_noti_to_admin(user_id, overall_data, date) + Helper.response_json_with_data(true, "Nhập thông tin thành công!", overall_data) + _ -> Helper.response_json_message(false, "Có lỗi xảy ra", 406) end @@ -101,7 +106,9 @@ defmodule CecrUnwomenWeb.ContributionController do Helper.aggregate_with_fields(OverallHouseholdContribution, keys) end) |> case do - {:ok, overall_data} -> Helper.response_json_with_data(true, "Nhập thông tin thành công!", overall_data) + {:ok, overall_data} -> + send_noti_to_admin(user_id, overall_data, date) + Helper.response_json_with_data(true, "Nhập thông tin thành công!", overall_data) _ -> Helper.response_json_message(false, "Có lỗi xảy ra", 406) end @@ -110,6 +117,66 @@ defmodule CecrUnwomenWeb.ContributionController do json(conn, res) end + + defp send_noti_to_admin(user_id, data, date) do + user_info = from( + u in User, + where: u.id == ^user_id, + select: %{ + "first_name" => u.first_name, + "last_name" => u.last_name, + "role_id" => u.role_id, + "avatar_url" => u.avatar_url + } + ) + |> Repo.one + + admin_fcm_tokens = from( + u in User, + join: ft in FirebaseToken, + on: u.id == ft.user_id, + where: u.role_id == 1, + select: %{ + "user_id" => u.id, + "token" => ft.token, + } + ) + |> Repo.all + # fold cac token thuoc cung 1 user lai ve 1 map + |> Helper.fold_fcm_token() + + Enum.each(admin_fcm_tokens, fn t -> + tokens = t["tokens"] + date_string = Calendar.strftime(date, "%d/%m/%Y") + user_name = "#{user_info["first_name"]} #{user_info["last_name"]}" + role_name = if (user_info["role_id"] == 2), do: "hộ gia đình", else: "người thu gom" + role_id = user_info["role_id"] + avatar_url = user_info["avatar_url"] + + FcmWorker.send_firebase_notification( + Enum.map(tokens, fn t -> %{"token" => t} end), + %{ + "title" => "#{user_name} (#{role_name}) vừa nhập dữ liệu ngày #{date_string}", + "body" => "Có dữ liệu đóng góp mới. Ấn vào thông báo để xem thông tin" + }, + %{ + "type" => "user_contribute_data", + "date" => date, + "formatted_date" => date_string, + "name" => user_name, + "role_id" => role_id, + "user_id" => user_id, + "avatar_url" => avatar_url, + "kg_co2e_reduced" => Map.get(data,:kg_co2e_reduced), + "kg_collected" => Map.get(data,:kg_collected), + "expense_reduced" => Map.get(data,:expense_reduced), + "kg_co2e_plastic_reduced" => Map.get(data, :kg_co2e_plastic_reduced), + "kg_co2e_recycle_reduced" => Map.get(data, :kg_co2e_recycle_reduced), + "kg_recycle_collected" => Map.get(data,:kg_recycle_collected), + } + ) + end) + end def edit_factor_quantity(conn, params) do user_id = conn.assigns.user.user_id diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 3dcaaa7..a8d0010 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -16,7 +16,7 @@ if (keystorePropertiesFile.exists()) { android { namespace = "vn.sparc.waznet" - compileSdk = flutter.compileSdkVersion + compileSdk = 34 ndkVersion = flutter.ndkVersion compileOptions { @@ -40,7 +40,7 @@ android { ndk { debugSymbolLevel 'FULL' } - + multiDexEnabled true } signingConfigs { diff --git a/mobile/ios/Runner/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift index 3ef543a..6bded4a 100644 --- a/mobile/ios/Runner/AppDelegate.swift +++ b/mobile/ios/Runner/AppDelegate.swift @@ -3,6 +3,7 @@ import UIKit import Firebase import FirebaseCore import FirebaseMessaging +import flutter_local_notifications @main @objc class AppDelegate: FlutterAppDelegate { @@ -10,7 +11,10 @@ import FirebaseMessaging _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - FirebaseApp.configure() + FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { (registry) in + GeneratedPluginRegistrant.register(with: registry) + } + FirebaseApp.configure() // UNUserNotificationCenter.current().delegate = self // // let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] @@ -21,6 +25,9 @@ import FirebaseMessaging // // application.registerForRemoteNotifications() GeneratedPluginRegistrant.register(with: self) + if #available(iOS 10.0, *) { + UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate + } return super.application(application, didFinishLaunchingWithOptions: launchOptions) } diff --git a/mobile/lib/features/firebase/bloc/firebase_bloc.dart b/mobile/lib/features/firebase/bloc/firebase_bloc.dart index a4cea0a..42c3d4b 100644 --- a/mobile/lib/features/firebase/bloc/firebase_bloc.dart +++ b/mobile/lib/features/firebase/bloc/firebase_bloc.dart @@ -1,7 +1,10 @@ +import 'dart:convert'; + import 'package:bloc/bloc.dart'; import 'package:cecr_unwomen/features/firebase/bloc/firebase_event.dart'; import 'package:cecr_unwomen/features/firebase/bloc/firebase_state.dart'; import 'package:cecr_unwomen/features/firebase/repository/firebase_repository.dart'; +import 'package:cecr_unwomen/service/notification_service.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; class FirebaseBloc extends Bloc{ @@ -33,7 +36,12 @@ class FirebaseBloc extends Bloc{ await emit.onEach(FirebaseMessaging.onMessage, onData: (RemoteMessage message) async { if (message.notification == null) return; - print('Message foreground also contained a notification: ${message.notification}'); + if (message.data.isNotEmpty && message.data["type"] != null) { + NotificationService.showNotification(message.notification!.title ?? "", message.notification!.body ?? "", jsonEncode(message.data)); + } + else { + NotificationService.showNotification(message.notification!.title ?? "", message.notification!.body ?? "", null); + } }, onError: (e, t) => emit(state.copyWith(FirebaseStatus.noToken)) ); @@ -42,7 +50,10 @@ class FirebaseBloc extends Bloc{ Future _onOpenMessageBackground(OpenMessageBackground event, Emitter emit) async { await emit.onEach(FirebaseMessaging.onMessageOpenedApp, onData: (RemoteMessage message) async { - print('_onOpenMessageBackground:${message.notification?.body}'); + if (message.notification == null) return; + if (message.data.isNotEmpty && message.data["type"] != null) { + notificationTapBackground(message.data); + } // emit(state.copyWith(FirebaseStatus.haveToken)); }, onError: (e, t) => emit(state.copyWith(FirebaseStatus.noToken)) @@ -51,6 +62,12 @@ class FirebaseBloc extends Bloc{ Future _onOpenMessageTerminated(OpenMessageTerminated event, Emitter emit) async { RemoteMessage? initialMessage = await FirebaseMessaging.instance.getInitialMessage(); - if (initialMessage != null) print('terminated noti:$initialMessage'); + + if (initialMessage != null) { + if (initialMessage.notification == null) return; + if (initialMessage.data.isNotEmpty && initialMessage.data["type"] != null) { + notificationTapBackground(initialMessage.data); + } + } } } \ No newline at end of file diff --git a/mobile/lib/features/home/view/home_screen.dart b/mobile/lib/features/home/view/home_screen.dart index 337c58a..be2b331 100644 --- a/mobile/lib/features/home/view/home_screen.dart +++ b/mobile/lib/features/home/view/home_screen.dart @@ -27,10 +27,10 @@ class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override - State createState() => _HomeScreenState(); + State createState() => HomeScreenState(); } -class _HomeScreenState extends State { +class HomeScreenState extends State { final ColorConstants colorCons = ColorConstants(); final ScrollController _scrollControllerHome = ScrollController(); bool isHouseholdTab = true; @@ -38,6 +38,7 @@ class _HomeScreenState extends State { final Map householdData = {}; final Map scraperData = {}; bool needGetDataChart = false; + bool needGetDataAdmin = false; changeBar() { setState(() { @@ -57,6 +58,8 @@ class _HomeScreenState extends State { @override void initState() { super.initState(); + Utils.checkUpdateApp(context); + Utils.globalContext = context; final User? user = context.read().state.user; if (user == null) return; isHouseholdTab = user.roleId != 3; @@ -163,32 +166,41 @@ class _HomeScreenState extends State { ), ), Expanded( - child: SingleChildScrollView( - controller: _scrollControllerHome, - child: Column( - children: [ - BarWidget(isHousehold: isHouseholdTab, changeBar: changeBar), - CardStatistic( - isHouseholdTab: isHouseholdTab, - statistic: allData - ), - Container( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - buildChart(), - if (roleId == 1) - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: StatisticScreen( - roleId: roleId, - isHouseHoldTabAdminScreen: isHouseholdTab, - ), - ) - ], + child: RefreshIndicator.adaptive( + onRefresh: () { + setState(() { + needGetDataAdmin = !needGetDataAdmin; + }); + return callApiGetOverallData(); + }, + child: SingleChildScrollView( + controller: _scrollControllerHome, + child: Column( + children: [ + BarWidget(isHousehold: isHouseholdTab, changeBar: changeBar), + CardStatistic( + isHouseholdTab: isHouseholdTab, + statistic: allData ), - ), - ] + Container( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + buildChart(), + if (roleId == 1) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: StatisticScreen( + roleId: roleId, + isHouseHoldTabAdminScreen: isHouseholdTab, + needGetDataAdmin: needGetDataAdmin, + ), + ) + ], + ), + ), + ] + ), ), ), ) @@ -201,15 +213,11 @@ class _HomeScreenState extends State { backgroundColor: const Color(0xFFF4F4F5), floatingActionButton: roleId != 1 && _currentIndex == 0 ? FloatingActionButton( shape: const CircleBorder(), - onPressed: () async { - setState(() { - needGetDataChart = false; - }); + onPressed: () async { + needGetDataChart = true; final bool? shouldCallApi = await Navigator.push(context, MaterialPageRoute(builder: (context) => ContributionScreen(roleId: roleId))); + needGetDataChart = false; if (!(shouldCallApi ?? false)) return; - setState(() { - needGetDataChart = true; - }); callApiGetOverallData(); }, backgroundColor: const Color(0xFF4CAF50), diff --git a/mobile/lib/features/home/view/statistic_screen.dart b/mobile/lib/features/home/view/statistic_screen.dart index f69ebee..9be44a0 100644 --- a/mobile/lib/features/home/view/statistic_screen.dart +++ b/mobile/lib/features/home/view/statistic_screen.dart @@ -3,16 +3,16 @@ import 'package:cecr_unwomen/features/home/view/component/header_widget.dart'; import 'package:cecr_unwomen/features/home/view/component/tab_bar_widget.dart'; import 'package:cecr_unwomen/features/home/view/contribution_screen.dart'; import 'package:cecr_unwomen/temp_api.dart'; -import 'package:cecr_unwomen/utils.dart'; import 'package:cecr_unwomen/widgets/filter_time.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; class StatisticScreen extends StatefulWidget { - const StatisticScreen({super.key, required this.roleId, this.isHouseHoldTabAdminScreen}); + const StatisticScreen({super.key, required this.roleId, this.isHouseHoldTabAdminScreen, this.needGetDataAdmin}); final int roleId; final bool? isHouseHoldTabAdminScreen; + final bool? needGetDataAdmin; @override State createState() => _StatisticScreenState(); @@ -43,6 +43,12 @@ class _StatisticScreenState extends State { ) { isHouseholdTab = widget.isHouseHoldTabAdminScreen!; } + + if (widget.needGetDataAdmin != null && oldWidget.needGetDataAdmin != null + && widget.needGetDataAdmin!= oldWidget.needGetDataAdmin + ) { + callApiGetFilterOverallData(); + } } callApiGetFilterOverallData({bool isCustomRange = false}) async { @@ -214,8 +220,14 @@ class _StatisticScreenState extends State { Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: SingleChildScrollView( - child: buildStatistic() + child: RefreshIndicator.adaptive( + + onRefresh: () { + return callApiGetFilterOverallData(); + }, + child: SingleChildScrollView( + child: buildStatistic() + ), ), ), ), diff --git a/mobile/lib/features/user/view/screen/app_info.dart b/mobile/lib/features/user/view/screen/app_info.dart index d48b780..0d4fdcc 100644 --- a/mobile/lib/features/user/view/screen/app_info.dart +++ b/mobile/lib/features/user/view/screen/app_info.dart @@ -1,7 +1,5 @@ import 'package:cecr_unwomen/constants/color_constants.dart'; import 'package:cecr_unwomen/features/home/view/component/header_widget.dart'; -import 'package:cecr_unwomen/utils.dart'; -import 'package:cecr_unwomen/widgets/navigation_button.dart'; import 'package:flutter/material.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; diff --git a/mobile/lib/features/user/view/user_info.dart b/mobile/lib/features/user/view/user_info.dart index 981e001..b09a2ac 100644 --- a/mobile/lib/features/user/view/user_info.dart +++ b/mobile/lib/features/user/view/user_info.dart @@ -67,6 +67,27 @@ class _UserInfoState extends State { } } + void showTimePicker() => Utils.showDatePicker( + context: context, + onCancel: () { + trackingTime = DateTime.now(); + Navigator.pop(context); + }, + onSave: () async { + setState(() { + remindedTime = trackingTime; + }); + Navigator.pop(context); + + updateTimeReminded(); + }, + onDateTimeChanged: (time) { + trackingTime = time; + }, + mode: CupertinoDatePickerMode.time, + initDate: user.timeReminded ?? trackingTime + ); + return SafeArea( child: Padding( padding: const EdgeInsets.only(bottom: 16), @@ -124,12 +145,16 @@ class _UserInfoState extends State { icon: PhosphorIcons.regular.alarm, hasSwitch: true, valueSwitch: user.timeReminded != null, - onToggleSwitch: remindedTime == null ? null : (toggle) async { - // tat switch -> reset time - if (!toggle && remindedTime != null) { - remindedTime = null; + onToggleSwitch: (toggle) async { + if (toggle) { + showTimePicker(); + } else { + // tat switch -> reset time + if (!toggle && remindedTime != null) { + remindedTime = null; + } + updateTimeReminded(); } - updateTimeReminded(); }, subTitleWidget: Column( children: [ @@ -146,26 +171,7 @@ class _UserInfoState extends State { children: [ Text("Thời gian", style: colorCons.fastStyle(16, FontWeight.w600, const Color(0xff333334)),), InkWell( - onTap: () => Utils.showDatePicker( - context: context, - onCancel: () { - trackingTime = DateTime.now(); - Navigator.pop(context); - }, - onSave: () async { - setState(() { - remindedTime = trackingTime; - }); - Navigator.pop(context); - - updateTimeReminded(); - }, - onDateTimeChanged: (time) { - trackingTime = time; - }, - mode: CupertinoDatePickerMode.time, - initDate: user.timeReminded ?? trackingTime - ), + onTap: showTimePicker, child: Text(remindedTime == null ? "--:--" : DateFormat("HH:mm").format(remindedTime!) , style: colorCons.fastStyle(14, FontWeight.w500, const Color(0xff4CAF50)), ) diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 0931c73..2197ff6 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -1,5 +1,7 @@ import 'package:cecr_unwomen/features/login/view/login_screen.dart'; import 'package:cecr_unwomen/features/home/view/home_screen.dart'; +import 'package:cecr_unwomen/service/notification_service.dart'; +import 'package:cecr_unwomen/utils.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:firebase_core/firebase_core.dart'; @@ -10,6 +12,7 @@ import 'package:flutter_localizations/flutter_localizations.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + NotificationService.init(); await Firebase.initializeApp(); FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); NotificationSettings settings = await FirebaseMessaging.instance.requestPermission( @@ -21,20 +24,14 @@ void main() async { provisional: false, sound: true, ); - await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(alert: true,badge: true,sound: true); + // await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(); + // da listen trong firebase bloc + // FirebaseMessaging.onMessage.listen((RemoteMessage message) { + // // print('Message data: ${message.toMap()}'); + // if (message.notification != null) { + // NotificationService.showNotification(message.notification!.title ?? "", message.notification!.body ?? ""); + // }}); - // if (settings.authorizationStatus == AuthorizationStatus.authorized) { - // String? token = await FirebaseMessaging.instance.getToken(); - // print("token: $token"); - // FirebaseMessaging.onMessage.listen((RemoteMessage message) { - // print('Got a message whilst in the foreground!'); - // print('Message data: ${message.data}'); - - // if (message.notification != null) { - // print('Message also contained a notification: ${message.notification}'); - // } - // }); - // } runApp(const App()); } @@ -95,7 +92,7 @@ class BlocEntireApp extends StatelessWidget { return BlocBuilder( builder: (context, state) { if (state.status == AuthenticationStatus.authorized) { - return const HomeScreen(); + return HomeScreen(key: Utils.globalHomeKey); } else { return const LoginScreen(); } diff --git a/mobile/lib/service/notification_service.dart b/mobile/lib/service/notification_service.dart new file mode 100644 index 0000000..d1d2d39 --- /dev/null +++ b/mobile/lib/service/notification_service.dart @@ -0,0 +1,98 @@ +import 'dart:convert'; + +import 'package:cecr_unwomen/features/home/view/component/modal/user_contribution_detail.dart'; +import 'package:cecr_unwomen/features/home/view/contribution_screen.dart'; +import 'package:cecr_unwomen/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; + +@pragma('vm:entry-point') +void notificationTapBackground(data) async { + // handle action + switch (data["type"]) { + case "user_contribute_data": + Map oneDayData = data["role_id"] == 3 ? { + "kg_co2e_plastic_reduced": double.tryParse(data["kg_co2e_plastic_reduced"]) ?? 0, + "kg_co2e_recycle_reduced": double.tryParse(data["kg_co2e_recycle_reduced"]) ?? 0, + "kg_recycle_collected": double.tryParse(data["kg_recycle_collected"]) ?? 0, + "date": data["date"] + } : { + "kg_co2e_reduced": double.tryParse(data["kg_co2e_reduced"]) ?? 0, + "kg_collected": double.tryParse(data["kg_collected"]) ?? 0, + "expense_reduced": double.tryParse(data["expense_reduced"]) ?? 0, + "date": data["date"] + }; + Navigator.push(Utils.globalContext!, MaterialPageRoute( + builder: (context) => Material( + child: UserContributionDetailScreen( + oneDayData: oneDayData, + userId: data["user_id"], + name: data["name"], + avatarUrl: data["avatar_url"].isEmpty ? null : data["avatar_url"], + date: data["formatted_date"], + roleIdUser: int.parse(data["role_id"]) + ), + ) + ) + ); + break; + + case "remind_to_contribute": + int roleId = int.parse(data["role_id"]); + final homeScreenKey = Utils.globalHomeKey; + if (homeScreenKey.currentState == null) return; + if (homeScreenKey.currentState!.needGetDataChart) return; + homeScreenKey.currentState!.needGetDataChart = false; + + final bool? shouldCallApi = await Navigator.push(homeScreenKey.currentState!.context, MaterialPageRoute(builder: (context) => ContributionScreen(roleId: roleId))); + if (!(shouldCallApi ?? false)) return; + homeScreenKey.currentState!.needGetDataChart = true; + homeScreenKey.currentState!.callApiGetOverallData(); + break; + default: + return; + } +} +class NotificationService { + static final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + + static Future onDidReceiveNotificationResponse(NotificationResponse notificationResponse) async { + final data = jsonDecode(notificationResponse.payload!); + + if (data["type"] == null) return; + notificationTapBackground(data); + } + + static Future onDidReceiveBackgroundNotificationResponse(NotificationResponse notificationResponse) async { + // final data = jsonDecode(notificationResponse.payload!); + + // if (data["type"] == null) return; + // notificationTapBackground(data); + } + + static void init(){ + // android + const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/waznet_icon'); + // ios + const DarwinInitializationSettings initializationSettingsDarwin = DarwinInitializationSettings(); + + const InitializationSettings initializationSettings = InitializationSettings( + android: initializationSettingsAndroid, + iOS: initializationSettingsDarwin, + ); + + flutterLocalNotificationsPlugin.initialize(initializationSettings, + onDidReceiveNotificationResponse: onDidReceiveNotificationResponse, + onDidReceiveBackgroundNotificationResponse: onDidReceiveBackgroundNotificationResponse + ); + } + + static Future showNotification(String title, String body, String? payload) async { + NotificationDetails details = const NotificationDetails( + android: AndroidNotificationDetails("channel_id", "channel_name", importance: Importance.high, priority: Priority.high), + iOS: DarwinNotificationDetails() + ); + + await flutterLocalNotificationsPlugin.show((DateTime.now().millisecondsSinceEpoch / 10000).ceil(), title, body, details, payload: payload); + } +} \ No newline at end of file diff --git a/mobile/lib/utils.dart b/mobile/lib/utils.dart index be61530..705b2e6 100644 --- a/mobile/lib/utils.dart +++ b/mobile/lib/utils.dart @@ -1,11 +1,24 @@ +import 'dart:convert'; +import 'dart:io'; + import 'package:cecr_unwomen/constants/color_constants.dart'; +import 'package:cecr_unwomen/features/home/view/home_screen.dart'; import 'package:cecr_unwomen/secrets.dart'; +import 'package:dio/dio.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:in_app_update/in_app_update.dart'; import 'package:intl/intl.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:url_launcher/url_launcher.dart'; class Utils { + static BuildContext? globalContext; + static GlobalKey globalHomeKey = GlobalKey(); + static Future getFirebaseToken() async { final String? fcmToken = await FirebaseMessaging.instance.getToken(); return fcmToken; @@ -137,4 +150,79 @@ class Utils { ), ); } + + static openUrl(String? url) async{ + Uri uri = Uri.parse(url ?? ""); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } else { + throw 'Could not launch $url'; + } + } + + static Future checkUpdateApp(BuildContext context) async { + try { + SharedPreferences sharedPreferences = await SharedPreferences.getInstance(); + if (kDebugMode) return; + if (Platform.isAndroid){ + InAppUpdate.checkForUpdate().then((res) { + if (res.flexibleUpdateAllowed) { + InAppUpdate.startFlexibleUpdate(); + } + }); + } else { + String url = "https://itunes.apple.com/lookup?bundleId=vn.sparc.waznet"; + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + String versionCurrent = packageInfo.version; + if (sharedPreferences.getString("current_version") != versionCurrent) { + sharedPreferences.setString("current_version", versionCurrent); + sharedPreferences.setBool("show_update_dialog", true); + } + bool isShowDialogUpdate = sharedPreferences.getBool("show_update_dialog") ?? false; + var res = await Dio().get(url); + var versionCurrentStore = (jsonDecode(res.data))["results"][0]["version"]; + if (int.parse("${versionCurrentStore.split(".").join()}") > int.parse(versionCurrent.split(".").join()) && isShowDialogUpdate && context.mounted) { + return showDialog( + context: context, + builder: (BuildContext c) { + return CupertinoAlertDialog( + title: const Text("Có bản cập nhật mới", style: TextStyle(fontWeight: FontWeight.w600, fontFamily: "Inter", fontSize: 18)), + content: const Text("Vui lòng cập nhật ứng dụng để trải nghiệm thêm các tính năng mới", style: TextStyle(fontFamily: "Inter", fontSize: 14),), + actions: [ + CupertinoDialogAction( + child: const Text( + "Cập nhật", + style: TextStyle(fontWeight: FontWeight.w600, color: Colors.blueAccent, fontFamily: "Inter",), + ), + onPressed: () { + Navigator.pop(context); + Utils.openUrl("https://apps.apple.com/us/app/waznet/id6738925384"); + }, + ), + CupertinoDialogAction( + child: const Text( + "Không nhắc lại", + style: TextStyle(fontWeight: FontWeight.w600, color: Colors.blueAccent, fontFamily: "Inter",), + ), + onPressed: () { + sharedPreferences.setBool("show_update_dialog", false); + Navigator.pop(context); + }, + ), + CupertinoDialogAction( + child: const Text( + "Để sau", + style: TextStyle(fontWeight: FontWeight.w600, color: Colors.blueAccent, fontFamily: "Inter",), + ), + onPressed: () => Navigator.pop(context), + ) + ] + ); + }); + } + } + } catch (e, t) { + print("_________________________________________________checkUpdateApp Error: $e \n $t"); + } + } } \ No newline at end of file diff --git a/mobile/lib/widgets/filter_time.dart b/mobile/lib/widgets/filter_time.dart index d7a78aa..e771c7d 100644 --- a/mobile/lib/widgets/filter_time.dart +++ b/mobile/lib/widgets/filter_time.dart @@ -44,6 +44,12 @@ class _TimeFilterState extends State { } } + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Material( diff --git a/mobile/linux/flutter/generated_plugin_registrant.cc b/mobile/linux/flutter/generated_plugin_registrant.cc index 10e19fe..cf6d642 100644 --- a/mobile/linux/flutter/generated_plugin_registrant.cc +++ b/mobile/linux/flutter/generated_plugin_registrant.cc @@ -7,9 +7,13 @@ #include "generated_plugin_registrant.h" #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_localization_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterLocalizationPlugin"); flutter_localization_plugin_register_with_registrar(flutter_localization_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/mobile/linux/flutter/generated_plugins.cmake b/mobile/linux/flutter/generated_plugins.cmake index 2284757..2ceb801 100644 --- a/mobile/linux/flutter/generated_plugins.cmake +++ b/mobile/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_localization + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/mobile/macos/Flutter/GeneratedPluginRegistrant.swift b/mobile/macos/Flutter/GeneratedPluginRegistrant.swift index 2c0f94f..7839ddc 100644 --- a/mobile/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/mobile/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,12 +7,18 @@ import Foundation import firebase_core import firebase_messaging +import flutter_local_notifications import flutter_localization +import package_info_plus import shared_preferences_foundation +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) + FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterLocalizationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalizationPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 99a39f5..ecee256 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -3,7 +3,7 @@ description: "WazNet (Waste zero, Net zero) is a mobile app that helps users tra # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.0.3+26 +version: 1.0.3+27 environment: sdk: ^3.5.3 @@ -35,6 +35,10 @@ dependencies: fl_chart: ^0.69.2 syncfusion_flutter_charts: ^20.4.54 syncfusion_flutter_datepicker: ^20.4.54 + url_launcher: ^6.3.1 + package_info_plus: ^8.1.2 + in_app_update: ^4.2.3 + flutter_local_notifications: ^18.0.0 dev_dependencies: flutter_test: diff --git a/mobile/windows/flutter/generated_plugin_registrant.cc b/mobile/windows/flutter/generated_plugin_registrant.cc index 0a04f2f..1943753 100644 --- a/mobile/windows/flutter/generated_plugin_registrant.cc +++ b/mobile/windows/flutter/generated_plugin_registrant.cc @@ -8,10 +8,13 @@ #include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { FirebaseCorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); FlutterLocalizationPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterLocalizationPluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/mobile/windows/flutter/generated_plugins.cmake b/mobile/windows/flutter/generated_plugins.cmake index b70a297..a712f02 100644 --- a/mobile/windows/flutter/generated_plugins.cmake +++ b/mobile/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST firebase_core flutter_localization + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST